added basic ingredient editor
This commit is contained in:
parent
2ee96c2ea4
commit
7befa4a084
@ -42,7 +42,8 @@ class ExtendedRecipeMixin(serializers.ModelSerializer):
|
|||||||
api_serializer = None
|
api_serializer = None
|
||||||
# extended values are computationally expensive and not needed in normal circumstances
|
# extended values are computationally expensive and not needed in normal circumstances
|
||||||
try:
|
try:
|
||||||
if str2bool(self.context['request'].query_params.get('extended', False)) and self.__class__ == api_serializer:
|
if str2bool(
|
||||||
|
self.context['request'].query_params.get('extended', False)) and self.__class__ == api_serializer:
|
||||||
return fields
|
return fields
|
||||||
except (AttributeError, KeyError) as e:
|
except (AttributeError, KeyError) as e:
|
||||||
pass
|
pass
|
||||||
@ -96,7 +97,8 @@ class CustomOnHandField(serializers.Field):
|
|||||||
shared_users = getattr(request, '_shared_users', None)
|
shared_users = getattr(request, '_shared_users', None)
|
||||||
if shared_users is None:
|
if shared_users is None:
|
||||||
try:
|
try:
|
||||||
shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [self.context['request'].user.id]
|
shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [
|
||||||
|
self.context['request'].user.id]
|
||||||
except AttributeError: # Anonymous users (using share links) don't have shared users
|
except AttributeError: # Anonymous users (using share links) don't have shared users
|
||||||
shared_users = []
|
shared_users = []
|
||||||
return obj.onhand_users.filter(id__in=shared_users).exists()
|
return obj.onhand_users.filter(id__in=shared_users).exists()
|
||||||
@ -170,7 +172,8 @@ class FoodInheritFieldSerializer(WritableNestedModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class UserPreferenceSerializer(WritableNestedModelSerializer):
|
class UserPreferenceSerializer(WritableNestedModelSerializer):
|
||||||
food_inherit_default = FoodInheritFieldSerializer(source='space.food_inherit', many=True, allow_null=True, required=False, read_only=True)
|
food_inherit_default = FoodInheritFieldSerializer(source='space.food_inherit', many=True, allow_null=True,
|
||||||
|
required=False, read_only=True)
|
||||||
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)
|
||||||
shopping_share = UserNameSerializer(many=True, allow_null=True, required=False)
|
shopping_share = UserNameSerializer(many=True, allow_null=True, required=False)
|
||||||
food_children_exist = serializers.SerializerMethodField('get_food_children_exist')
|
food_children_exist = serializers.SerializerMethodField('get_food_children_exist')
|
||||||
@ -189,9 +192,12 @@ class UserPreferenceSerializer(WritableNestedModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = UserPreference
|
model = UserPreference
|
||||||
fields = (
|
fields = (
|
||||||
'user', 'theme', 'nav_color', 'default_unit', 'default_page', 'use_fractions', 'use_kj', 'search_style', 'show_recent', 'plan_share',
|
'user', 'theme', 'nav_color', 'default_unit', 'default_page', 'use_fractions', 'use_kj', 'search_style',
|
||||||
'ingredient_decimals', 'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'food_inherit_default', 'default_delay',
|
'show_recent', 'plan_share',
|
||||||
'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share', 'shopping_recent_days', 'csv_delim', 'csv_prefix',
|
'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', 'shopping_add_onhand', 'left_handed', 'food_children_exist'
|
'filter_to_supermarket', 'shopping_add_onhand', 'left_handed', 'food_children_exist'
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -393,7 +399,6 @@ class FoodSimpleSerializer(serializers.ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Food
|
model = Food
|
||||||
fields = ('id', 'name')
|
fields = ('id', 'name')
|
||||||
read_only_fields = ['id', 'name']
|
|
||||||
|
|
||||||
|
|
||||||
class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedRecipeMixin):
|
class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedRecipeMixin):
|
||||||
@ -416,7 +421,8 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
|||||||
shared_users = getattr(request, '_shared_users', None)
|
shared_users = getattr(request, '_shared_users', None)
|
||||||
if shared_users is None:
|
if shared_users is None:
|
||||||
try:
|
try:
|
||||||
shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [self.context['request'].user.id]
|
shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [
|
||||||
|
self.context['request'].user.id]
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
shared_users = []
|
shared_users = []
|
||||||
filter = Q(id__in=obj.substitute.all())
|
filter = Q(id__in=obj.substitute.all())
|
||||||
@ -487,8 +493,8 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
|||||||
read_only_fields = ('id', 'numchild', 'parent', 'image', 'numrecipe')
|
read_only_fields = ('id', 'numchild', 'parent', 'image', 'numrecipe')
|
||||||
|
|
||||||
|
|
||||||
class IngredientSerializer(WritableNestedModelSerializer):
|
class IngredientSimpleSerializer(WritableNestedModelSerializer):
|
||||||
food = FoodSerializer(allow_null=True)
|
food = FoodSimpleSerializer(allow_null=True)
|
||||||
unit = UnitSerializer(allow_null=True)
|
unit = UnitSerializer(allow_null=True)
|
||||||
amount = CustomDecimalField()
|
amount = CustomDecimalField()
|
||||||
|
|
||||||
@ -508,6 +514,10 @@ class IngredientSerializer(WritableNestedModelSerializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class IngredientSerializer(IngredientSimpleSerializer):
|
||||||
|
food = FoodSerializer(allow_null=True)
|
||||||
|
|
||||||
|
|
||||||
class StepSerializer(WritableNestedModelSerializer, ExtendedRecipeMixin):
|
class StepSerializer(WritableNestedModelSerializer, ExtendedRecipeMixin):
|
||||||
ingredients = IngredientSerializer(many=True)
|
ingredients = IngredientSerializer(many=True)
|
||||||
ingredients_markdown = serializers.SerializerMethodField('get_ingredients_markdown')
|
ingredients_markdown = serializers.SerializerMethodField('get_ingredients_markdown')
|
||||||
@ -699,7 +709,8 @@ class RecipeBookEntrySerializer(serializers.ModelSerializer):
|
|||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
book = validated_data['book']
|
book = validated_data['book']
|
||||||
recipe = validated_data['recipe']
|
recipe = validated_data['recipe']
|
||||||
if not book.get_owner() == self.context['request'].user and not self.context['request'].user in book.get_shared():
|
if not book.get_owner() == self.context['request'].user and not self.context[
|
||||||
|
'request'].user in book.get_shared():
|
||||||
raise NotFound(detail=None, code=None)
|
raise NotFound(detail=None, code=None)
|
||||||
obj, created = RecipeBookEntry.objects.get_or_create(book=book, recipe=recipe)
|
obj, created = RecipeBookEntry.objects.get_or_create(book=book, recipe=recipe)
|
||||||
return obj
|
return obj
|
||||||
@ -752,7 +763,8 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer):
|
|||||||
def get_name(self, obj):
|
def get_name(self, obj):
|
||||||
if not isinstance(value := obj.servings, Decimal):
|
if not isinstance(value := obj.servings, Decimal):
|
||||||
value = Decimal(value)
|
value = Decimal(value)
|
||||||
value = value.quantize(Decimal(1)) if value == value.to_integral() else value.normalize() # strips trailing zero
|
value = value.quantize(
|
||||||
|
Decimal(1)) if value == value.to_integral() else value.normalize() # strips trailing zero
|
||||||
return (
|
return (
|
||||||
obj.name
|
obj.name
|
||||||
or getattr(obj.mealplan, 'title', None)
|
or getattr(obj.mealplan, 'title', None)
|
||||||
@ -829,7 +841,8 @@ class ShoppingListEntrySerializer(WritableNestedModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = ShoppingListEntry
|
model = ShoppingListEntry
|
||||||
fields = (
|
fields = (
|
||||||
'id', 'list_recipe', 'food', 'unit', 'ingredient', 'ingredient_note', 'amount', 'order', 'checked', 'recipe_mealplan',
|
'id', 'list_recipe', 'food', 'unit', 'ingredient', 'ingredient_note', 'amount', 'order', 'checked',
|
||||||
|
'recipe_mealplan',
|
||||||
'created_by', 'created_at', 'completed_at', 'delay_until'
|
'created_by', 'created_at', 'completed_at', 'delay_until'
|
||||||
)
|
)
|
||||||
read_only_fields = ('id', 'created_by', 'created_at',)
|
read_only_fields = ('id', 'created_by', 'created_at',)
|
||||||
@ -927,7 +940,10 @@ class ExportLogSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ExportLog
|
model = ExportLog
|
||||||
fields = ('id', 'type', 'msg', 'running', 'total_recipes', 'exported_recipes', 'cache_duration', 'possibly_not_expired', 'created_by', 'created_at')
|
fields = (
|
||||||
|
'id', 'type', 'msg', 'running', 'total_recipes', 'exported_recipes', 'cache_duration',
|
||||||
|
'possibly_not_expired',
|
||||||
|
'created_by', 'created_at')
|
||||||
read_only_fields = ('created_by',)
|
read_only_fields = ('created_by',)
|
||||||
|
|
||||||
|
|
||||||
@ -1039,10 +1055,12 @@ class RecipeExportSerializer(WritableNestedModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class RecipeShoppingUpdateSerializer(serializers.ModelSerializer):
|
class RecipeShoppingUpdateSerializer(serializers.ModelSerializer):
|
||||||
list_recipe = serializers.IntegerField(write_only=True, allow_null=True, required=False, help_text=_("Existing shopping list to update"))
|
list_recipe = serializers.IntegerField(write_only=True, allow_null=True, required=False,
|
||||||
|
help_text=_("Existing shopping list to update"))
|
||||||
ingredients = serializers.IntegerField(write_only=True, allow_null=True, required=False, help_text=_(
|
ingredients = serializers.IntegerField(write_only=True, allow_null=True, required=False, help_text=_(
|
||||||
"List of ingredient IDs from the recipe to add, if not provided all ingredients will be added."))
|
"List of ingredient IDs from the recipe to add, if not provided all ingredients will be added."))
|
||||||
servings = serializers.IntegerField(default=1, write_only=True, allow_null=True, required=False, help_text=_("Providing a list_recipe ID and servings of 0 will delete that shopping list."))
|
servings = serializers.IntegerField(default=1, write_only=True, allow_null=True, required=False, help_text=_(
|
||||||
|
"Providing a list_recipe ID and servings of 0 will delete that shopping list."))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Recipe
|
model = Recipe
|
||||||
@ -1050,9 +1068,12 @@ class RecipeShoppingUpdateSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class FoodShoppingUpdateSerializer(serializers.ModelSerializer):
|
class FoodShoppingUpdateSerializer(serializers.ModelSerializer):
|
||||||
amount = serializers.IntegerField(write_only=True, allow_null=True, required=False, help_text=_("Amount of food to add to the shopping list"))
|
amount = serializers.IntegerField(write_only=True, allow_null=True, required=False,
|
||||||
unit = serializers.IntegerField(write_only=True, allow_null=True, required=False, help_text=_("ID of unit to use for the shopping list"))
|
help_text=_("Amount of food to add to the shopping list"))
|
||||||
delete = serializers.ChoiceField(choices=['true'], write_only=True, allow_null=True, allow_blank=True, help_text=_("When set to true will delete all food from active shopping lists."))
|
unit = serializers.IntegerField(write_only=True, allow_null=True, required=False,
|
||||||
|
help_text=_("ID of unit to use for the shopping list"))
|
||||||
|
delete = serializers.ChoiceField(choices=['true'], write_only=True, allow_null=True, allow_blank=True,
|
||||||
|
help_text=_("When set to true will delete all food from active shopping lists."))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Recipe
|
model = Recipe
|
||||||
|
31
cookbook/templates/ingredient_editor.html
Normal file
31
cookbook/templates/ingredient_editor.html
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load render_bundle from webpack_loader %}
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load l10n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans 'Ingredient Editor' %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div id="app">
|
||||||
|
<ingredient-editor-view></ingredient-editor-view>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
{% if debug %}
|
||||||
|
<script src="{% url 'js_reverse' %}"></script>
|
||||||
|
{% else %}
|
||||||
|
<script src="{% static 'django_js_reverse/reverse.js' %}"></script>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<script type="application/javascript">
|
||||||
|
window.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% render_bundle 'ingredient_editor_view' %}
|
||||||
|
{% endblock %}
|
@ -47,7 +47,6 @@ router.register(r'user-name', api.UserNameViewSet, basename='username')
|
|||||||
router.register(r'user-preference', api.UserPreferenceViewSet)
|
router.register(r'user-preference', api.UserPreferenceViewSet)
|
||||||
router.register(r'view-log', api.ViewLogViewSet)
|
router.register(r'view-log', api.ViewLogViewSet)
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', views.index, name='index'),
|
path('', views.index, name='index'),
|
||||||
path('setup/', views.setup, name='view_setup'),
|
path('setup/', views.setup, name='view_setup'),
|
||||||
@ -72,6 +71,7 @@ urlpatterns = [
|
|||||||
path('settings/', views.user_settings, name='view_settings'),
|
path('settings/', views.user_settings, name='view_settings'),
|
||||||
path('history/', views.history, name='view_history'),
|
path('history/', views.history, name='view_history'),
|
||||||
path('supermarket/', views.supermarket, name='view_supermarket'),
|
path('supermarket/', views.supermarket, name='view_supermarket'),
|
||||||
|
path('ingredient-editor/', views.ingredient_editor, name='view_ingredient_editor'),
|
||||||
path('abuse/<slug:token>', views.report_share_abuse, name='view_report_share_abuse'),
|
path('abuse/<slug:token>', views.report_share_abuse, name='view_report_share_abuse'),
|
||||||
|
|
||||||
path('import/', import_export.import_recipe, name='view_import'),
|
path('import/', import_export.import_recipe, name='view_import'),
|
||||||
@ -116,7 +116,8 @@ urlpatterns = [
|
|||||||
path('api/share-link/<int:pk>', api.share_link, name='api_share_link'),
|
path('api/share-link/<int:pk>', api.share_link, name='api_share_link'),
|
||||||
path('api/get_facets/', api.get_facets, name='api_get_facets'),
|
path('api/get_facets/', api.get_facets, name='api_get_facets'),
|
||||||
|
|
||||||
path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'), # TODO is this deprecated? not yet, some old forms remain, could likely be changed to generic API endpoints
|
path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'),
|
||||||
|
# TODO is this deprecated? not yet, some old forms remain, could likely be changed to generic API endpoints
|
||||||
path('dal/food/', dal.IngredientsAutocomplete.as_view(), name='dal_food'), # TODO is this deprecated?
|
path('dal/food/', dal.IngredientsAutocomplete.as_view(), name='dal_food'), # TODO is this deprecated?
|
||||||
path('dal/unit/', dal.UnitAutocomplete.as_view(), name='dal_unit'), # TODO is this deprecated?
|
path('dal/unit/', dal.UnitAutocomplete.as_view(), name='dal_unit'), # TODO is this deprecated?
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ from cookbook.serializer import (AutomationSerializer, BookmarkletImportSerializ
|
|||||||
SupermarketCategorySerializer, SupermarketSerializer,
|
SupermarketCategorySerializer, SupermarketSerializer,
|
||||||
SyncLogSerializer, SyncSerializer, UnitSerializer,
|
SyncLogSerializer, SyncSerializer, UnitSerializer,
|
||||||
UserFileSerializer, UserNameSerializer, UserPreferenceSerializer,
|
UserFileSerializer, UserNameSerializer, UserPreferenceSerializer,
|
||||||
ViewLogSerializer)
|
ViewLogSerializer, IngredientSimpleSerializer)
|
||||||
from recipes import settings
|
from recipes import settings
|
||||||
|
|
||||||
|
|
||||||
@ -119,13 +119,16 @@ class ExtendedRecipeMixin():
|
|||||||
|
|
||||||
# add a recipe count annotation to the query
|
# add a recipe count annotation to the query
|
||||||
# explanation on construction https://stackoverflow.com/a/43771738/15762829
|
# explanation on construction https://stackoverflow.com/a/43771738/15762829
|
||||||
recipe_count = Recipe.objects.filter(**{recipe_filter: OuterRef('id')}, space=space).values(recipe_filter).annotate(count=Count('pk')).values('count')
|
recipe_count = Recipe.objects.filter(**{recipe_filter: OuterRef('id')}, space=space).values(
|
||||||
|
recipe_filter).annotate(count=Count('pk')).values('count')
|
||||||
queryset = queryset.annotate(recipe_count=Coalesce(Subquery(recipe_count), 0))
|
queryset = queryset.annotate(recipe_count=Coalesce(Subquery(recipe_count), 0))
|
||||||
|
|
||||||
# add a recipe image annotation to the query
|
# add a recipe image annotation to the query
|
||||||
image_subquery = Recipe.objects.filter(**{recipe_filter: OuterRef('id')}, space=space).exclude(image__isnull=True).exclude(image__exact='').order_by("?").values('image')[:1]
|
image_subquery = Recipe.objects.filter(**{recipe_filter: OuterRef('id')}, space=space).exclude(
|
||||||
|
image__isnull=True).exclude(image__exact='').order_by("?").values('image')[:1]
|
||||||
if tree:
|
if tree:
|
||||||
image_children_subquery = Recipe.objects.filter(**{f"{recipe_filter}__path__startswith": OuterRef('path')},
|
image_children_subquery = Recipe.objects.filter(
|
||||||
|
**{f"{recipe_filter}__path__startswith": OuterRef('path')},
|
||||||
space=space).exclude(image__isnull=True).exclude(image__exact='').order_by("?").values('image')[:1]
|
space=space).exclude(image__isnull=True).exclude(image__exact='').order_by("?").values('image')[:1]
|
||||||
else:
|
else:
|
||||||
image_children_subquery = None
|
image_children_subquery = None
|
||||||
@ -142,11 +145,14 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
self.queryset = self.queryset.filter(space=self.request.space).order_by(Lower('name').asc())
|
self.queryset = self.queryset.filter(space=self.request.space).order_by(Lower('name').asc())
|
||||||
query = self.request.query_params.get('query', None)
|
query = self.request.query_params.get('query', None)
|
||||||
fuzzy = self.request.user.searchpreference.lookup or any([self.model.__name__.lower() in x for x in self.request.user.searchpreference.trigram.values_list('field', flat=True)])
|
fuzzy = self.request.user.searchpreference.lookup or any([self.model.__name__.lower() in x for x in
|
||||||
|
self.request.user.searchpreference.trigram.values_list(
|
||||||
|
'field', flat=True)])
|
||||||
|
|
||||||
if query is not None and query not in ["''", '']:
|
if query is not None and query not in ["''", '']:
|
||||||
if fuzzy:
|
if fuzzy:
|
||||||
if any([self.model.__name__.lower() in x for x in self.request.user.searchpreference.unaccent.values_list('field', flat=True)]):
|
if any([self.model.__name__.lower() in x for x in
|
||||||
|
self.request.user.searchpreference.unaccent.values_list('field', flat=True)]):
|
||||||
self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name__unaccent', query))
|
self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name__unaccent', query))
|
||||||
else:
|
else:
|
||||||
self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name', query))
|
self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name', query))
|
||||||
@ -154,7 +160,8 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
|
|||||||
else:
|
else:
|
||||||
# TODO have this check unaccent search settings or other search preferences?
|
# TODO have this check unaccent search settings or other search preferences?
|
||||||
filter = Q(name__icontains=query)
|
filter = Q(name__icontains=query)
|
||||||
if any([self.model.__name__.lower() in x for x in self.request.user.searchpreference.unaccent.values_list('field', flat=True)]):
|
if any([self.model.__name__.lower() in x for x in
|
||||||
|
self.request.user.searchpreference.unaccent.values_list('field', flat=True)]):
|
||||||
filter |= Q(name__unaccent__icontains=query)
|
filter |= Q(name__unaccent__icontains=query)
|
||||||
|
|
||||||
self.queryset = (
|
self.queryset = (
|
||||||
@ -275,10 +282,12 @@ class TreeMixin(MergeMixin, FuzzyFilterMixin, ExtendedRecipeMixin):
|
|||||||
except self.model.DoesNotExist:
|
except self.model.DoesNotExist:
|
||||||
self.queryset = self.model.objects.none()
|
self.queryset = self.model.objects.none()
|
||||||
else:
|
else:
|
||||||
return self.annotate_recipe(queryset=super().get_queryset(), request=self.request, serializer=self.serializer_class, tree=True)
|
return self.annotate_recipe(queryset=super().get_queryset(), request=self.request,
|
||||||
|
serializer=self.serializer_class, tree=True)
|
||||||
self.queryset = self.queryset.filter(space=self.request.space).order_by(Lower('name').asc())
|
self.queryset = self.queryset.filter(space=self.request.space).order_by(Lower('name').asc())
|
||||||
|
|
||||||
return self.annotate_recipe(queryset=self.queryset, request=self.request, serializer=self.serializer_class, tree=True)
|
return self.annotate_recipe(queryset=self.queryset, request=self.request, serializer=self.serializer_class,
|
||||||
|
tree=True)
|
||||||
|
|
||||||
@decorators.action(detail=True, url_path='move/(?P<parent>[^/.]+)', methods=['PUT'], )
|
@decorators.action(detail=True, url_path='move/(?P<parent>[^/.]+)', methods=['PUT'], )
|
||||||
@decorators.renderer_classes((TemplateHTMLRenderer, JSONRenderer))
|
@decorators.renderer_classes((TemplateHTMLRenderer, JSONRenderer))
|
||||||
@ -454,12 +463,16 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
|
|||||||
pagination_class = DefaultPagination
|
pagination_class = DefaultPagination
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
self.request._shared_users = [x.id for x in list(self.request.user.get_shopping_share())] + [self.request.user.id]
|
self.request._shared_users = [x.id for x in list(self.request.user.get_shopping_share())] + [
|
||||||
|
self.request.user.id]
|
||||||
|
|
||||||
self.queryset = super().get_queryset()
|
self.queryset = super().get_queryset()
|
||||||
shopping_status = ShoppingListEntry.objects.filter(space=self.request.space, food=OuterRef('id'), checked=False).values('id')
|
shopping_status = ShoppingListEntry.objects.filter(space=self.request.space, food=OuterRef('id'),
|
||||||
|
checked=False).values('id')
|
||||||
# onhand_status = self.queryset.annotate(onhand_status=Exists(onhand_users_set__in=[shared_users]))
|
# onhand_status = self.queryset.annotate(onhand_status=Exists(onhand_users_set__in=[shared_users]))
|
||||||
return self.queryset.annotate(shopping_status=Exists(shopping_status)).prefetch_related('onhand_users', 'inherit_fields').select_related('recipe', 'supermarket_category')
|
return self.queryset.annotate(shopping_status=Exists(shopping_status)).prefetch_related('onhand_users',
|
||||||
|
'inherit_fields').select_related(
|
||||||
|
'recipe', 'supermarket_category')
|
||||||
|
|
||||||
@decorators.action(detail=True, methods=['PUT'], serializer_class=FoodShoppingUpdateSerializer, )
|
@decorators.action(detail=True, methods=['PUT'], serializer_class=FoodShoppingUpdateSerializer, )
|
||||||
# TODO DRF only allows one action in a decorator action without overriding get_operation_id_base() this should be PUT and DELETE probably
|
# TODO DRF only allows one action in a decorator action without overriding get_operation_id_base() this should be PUT and DELETE probably
|
||||||
@ -470,7 +483,8 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
|
|||||||
shared_users = list(self.request.user.get_shopping_share())
|
shared_users = list(self.request.user.get_shopping_share())
|
||||||
shared_users.append(request.user)
|
shared_users.append(request.user)
|
||||||
if request.data.get('_delete', False) == 'true':
|
if request.data.get('_delete', False) == 'true':
|
||||||
ShoppingListEntry.objects.filter(food=obj, checked=False, space=request.space, created_by__in=shared_users).delete()
|
ShoppingListEntry.objects.filter(food=obj, checked=False, space=request.space,
|
||||||
|
created_by__in=shared_users).delete()
|
||||||
content = {'msg': _(f'{obj.name} was removed from the shopping list.')}
|
content = {'msg': _(f'{obj.name} was removed from the shopping list.')}
|
||||||
return Response(content, status=status.HTTP_204_NO_CONTENT)
|
return Response(content, status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
@ -478,7 +492,8 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
|
|||||||
unit = request.data.get('unit', None)
|
unit = request.data.get('unit', None)
|
||||||
content = {'msg': _(f'{obj.name} was added to the shopping list.')}
|
content = {'msg': _(f'{obj.name} was added to the shopping list.')}
|
||||||
|
|
||||||
ShoppingListEntry.objects.create(food=obj, amount=amount, unit=unit, space=request.space, created_by=request.user)
|
ShoppingListEntry.objects.create(food=obj, amount=amount, unit=unit, space=request.space,
|
||||||
|
created_by=request.user)
|
||||||
return Response(content, status=status.HTTP_204_NO_CONTENT)
|
return Response(content, status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
def destroy(self, *args, **kwargs):
|
def destroy(self, *args, **kwargs):
|
||||||
@ -577,8 +592,22 @@ class IngredientViewSet(viewsets.ModelViewSet):
|
|||||||
serializer_class = IngredientSerializer
|
serializer_class = IngredientSerializer
|
||||||
permission_classes = [CustomIsUser]
|
permission_classes = [CustomIsUser]
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
if self.request and self.request.query_params.get('simple', False):
|
||||||
|
return IngredientSimpleSerializer
|
||||||
|
return IngredientSerializer
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.queryset.filter(step__recipe__space=self.request.space)
|
queryset = self.queryset.filter(step__recipe__space=self.request.space)
|
||||||
|
food = self.request.query_params.get('food', None)
|
||||||
|
if food and re.match(r'^([1-9])+$', food):
|
||||||
|
queryset = queryset.filter(food_id=food)
|
||||||
|
|
||||||
|
unit = self.request.query_params.get('unit', None)
|
||||||
|
if unit and re.match(r'^([1-9])+$', unit):
|
||||||
|
queryset = queryset.filter(unit_id=unit)
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class StepViewSet(viewsets.ModelViewSet):
|
class StepViewSet(viewsets.ModelViewSet):
|
||||||
@ -587,7 +616,8 @@ class StepViewSet(viewsets.ModelViewSet):
|
|||||||
permission_classes = [CustomIsUser]
|
permission_classes = [CustomIsUser]
|
||||||
pagination_class = DefaultPagination
|
pagination_class = DefaultPagination
|
||||||
query_params = [
|
query_params = [
|
||||||
QueryParam(name='recipe', description=_('ID of recipe a step is part of. For multiple repeat parameter.'), qtype='int'),
|
QueryParam(name='recipe', description=_('ID of recipe a step is part of. For multiple repeat parameter.'),
|
||||||
|
qtype='int'),
|
||||||
QueryParam(name='query', description=_('Query string matched (fuzzy) against object name.'), qtype='string'),
|
QueryParam(name='query', description=_('Query string matched (fuzzy) against object name.'), qtype='string'),
|
||||||
]
|
]
|
||||||
schema = QueryParamAutoSchema()
|
schema = QueryParamAutoSchema()
|
||||||
@ -631,33 +661,63 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
|||||||
pagination_class = RecipePagination
|
pagination_class = RecipePagination
|
||||||
|
|
||||||
query_params = [
|
query_params = [
|
||||||
QueryParam(name='query', description=_('Query string matched (fuzzy) against recipe name. In the future also fulltext search.')),
|
QueryParam(name='query', description=_(
|
||||||
QueryParam(name='keywords', description=_('ID of keyword a recipe should have. For multiple repeat parameter. Equivalent to keywords_or'), qtype='int'),
|
'Query string matched (fuzzy) against recipe name. In the future also fulltext search.')),
|
||||||
QueryParam(name='keywords_or', description=_('Keyword IDs, repeat for multiple. Return recipes with any of the keywords'), qtype='int'),
|
QueryParam(name='keywords', description=_(
|
||||||
QueryParam(name='keywords_and', description=_('Keyword IDs, repeat for multiple. Return recipes with all of the keywords.'), qtype='int'),
|
'ID of keyword a recipe should have. For multiple repeat parameter. Equivalent to keywords_or'),
|
||||||
QueryParam(name='keywords_or_not', description=_('Keyword IDs, repeat for multiple. Exclude recipes with any of the keywords.'), qtype='int'),
|
qtype='int'),
|
||||||
QueryParam(name='keywords_and_not', description=_('Keyword IDs, repeat for multiple. Exclude recipes with all of the keywords.'), qtype='int'),
|
QueryParam(name='keywords_or',
|
||||||
QueryParam(name='foods', description=_('ID of food a recipe should have. For multiple repeat parameter.'), qtype='int'),
|
description=_('Keyword IDs, repeat for multiple. Return recipes with any of the keywords'),
|
||||||
QueryParam(name='foods_or', description=_('Food IDs, repeat for multiple. Return recipes with any of the foods'), qtype='int'),
|
qtype='int'),
|
||||||
QueryParam(name='foods_and', description=_('Food IDs, repeat for multiple. Return recipes with all of the foods.'), qtype='int'),
|
QueryParam(name='keywords_and',
|
||||||
QueryParam(name='foods_or_not', description=_('Food IDs, repeat for multiple. Exclude recipes with any of the foods.'), qtype='int'),
|
description=_('Keyword IDs, repeat for multiple. Return recipes with all of the keywords.'),
|
||||||
QueryParam(name='foods_and_not', description=_('Food IDs, repeat for multiple. Exclude recipes with all of the foods.'), qtype='int'),
|
qtype='int'),
|
||||||
|
QueryParam(name='keywords_or_not',
|
||||||
|
description=_('Keyword IDs, repeat for multiple. Exclude recipes with any of the keywords.'),
|
||||||
|
qtype='int'),
|
||||||
|
QueryParam(name='keywords_and_not',
|
||||||
|
description=_('Keyword IDs, repeat for multiple. Exclude recipes with all of the keywords.'),
|
||||||
|
qtype='int'),
|
||||||
|
QueryParam(name='foods', description=_('ID of food a recipe should have. For multiple repeat parameter.'),
|
||||||
|
qtype='int'),
|
||||||
|
QueryParam(name='foods_or',
|
||||||
|
description=_('Food IDs, repeat for multiple. Return recipes with any of the foods'), qtype='int'),
|
||||||
|
QueryParam(name='foods_and',
|
||||||
|
description=_('Food IDs, repeat for multiple. Return recipes with all of the foods.'), qtype='int'),
|
||||||
|
QueryParam(name='foods_or_not',
|
||||||
|
description=_('Food IDs, repeat for multiple. Exclude recipes with any of the foods.'), qtype='int'),
|
||||||
|
QueryParam(name='foods_and_not',
|
||||||
|
description=_('Food IDs, repeat for multiple. Exclude recipes with all of the foods.'), qtype='int'),
|
||||||
QueryParam(name='units', description=_('ID of unit a recipe should have.'), qtype='int'),
|
QueryParam(name='units', description=_('ID of unit a recipe should have.'), qtype='int'),
|
||||||
QueryParam(name='rating', description=_('Rating a recipe should have or greater. [0 - 5] Negative value filters rating less than.'), qtype='int'),
|
QueryParam(name='rating', description=_(
|
||||||
|
'Rating a recipe should have or greater. [0 - 5] Negative value filters rating less than.'), qtype='int'),
|
||||||
QueryParam(name='books', description=_('ID of book a recipe should be in. For multiple repeat parameter.')),
|
QueryParam(name='books', description=_('ID of book a recipe should be in. For multiple repeat parameter.')),
|
||||||
QueryParam(name='books_or', description=_('Book IDs, repeat for multiple. Return recipes with any of the books'), qtype='int'),
|
QueryParam(name='books_or',
|
||||||
QueryParam(name='books_and', description=_('Book IDs, repeat for multiple. Return recipes with all of the books.'), qtype='int'),
|
description=_('Book IDs, repeat for multiple. Return recipes with any of the books'), qtype='int'),
|
||||||
QueryParam(name='books_or_not', description=_('Book IDs, repeat for multiple. Exclude recipes with any of the books.'), qtype='int'),
|
QueryParam(name='books_and',
|
||||||
QueryParam(name='books_and_not', description=_('Book IDs, repeat for multiple. Exclude recipes with all of the books.'), qtype='int'),
|
description=_('Book IDs, repeat for multiple. Return recipes with all of the books.'), qtype='int'),
|
||||||
QueryParam(name='internal', description=_('If only internal recipes should be returned. [''true''/''<b>false</b>'']')),
|
QueryParam(name='books_or_not',
|
||||||
QueryParam(name='random', description=_('Returns the results in randomized order. [''true''/''<b>false</b>'']')),
|
description=_('Book IDs, repeat for multiple. Exclude recipes with any of the books.'), qtype='int'),
|
||||||
QueryParam(name='new', description=_('Returns new results first in search results. [''true''/''<b>false</b>'']')),
|
QueryParam(name='books_and_not',
|
||||||
QueryParam(name='timescooked', description=_('Filter recipes cooked X times or more. Negative values returns cooked less than X times'), qtype='int'),
|
description=_('Book IDs, repeat for multiple. Exclude recipes with all of the books.'), qtype='int'),
|
||||||
QueryParam(name='cookedon', description=_('Filter recipes last cooked on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
QueryParam(name='internal',
|
||||||
QueryParam(name='createdon', description=_('Filter recipes created on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
description=_('If only internal recipes should be returned. [''true''/''<b>false</b>'']')),
|
||||||
QueryParam(name='updatedon', description=_('Filter recipes updated on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
QueryParam(name='random',
|
||||||
QueryParam(name='viewedon', description=_('Filter recipes lasts viewed on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
description=_('Returns the results in randomized order. [''true''/''<b>false</b>'']')),
|
||||||
QueryParam(name='makenow', description=_('Filter recipes that can be made with OnHand food. [''true''/''<b>false</b>'']')),
|
QueryParam(name='new',
|
||||||
|
description=_('Returns new results first in search results. [''true''/''<b>false</b>'']')),
|
||||||
|
QueryParam(name='timescooked', description=_(
|
||||||
|
'Filter recipes cooked X times or more. Negative values returns cooked less than X times'), qtype='int'),
|
||||||
|
QueryParam(name='cookedon', description=_(
|
||||||
|
'Filter recipes last cooked on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||||
|
QueryParam(name='createdon', description=_(
|
||||||
|
'Filter recipes created on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||||
|
QueryParam(name='updatedon', description=_(
|
||||||
|
'Filter recipes updated on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||||
|
QueryParam(name='viewedon', description=_(
|
||||||
|
'Filter recipes lasts viewed on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||||
|
QueryParam(name='makenow',
|
||||||
|
description=_('Filter recipes that can be made with OnHand food. [''true''/''<b>false</b>'']')),
|
||||||
]
|
]
|
||||||
schema = QueryParamAutoSchema()
|
schema = QueryParamAutoSchema()
|
||||||
|
|
||||||
@ -672,7 +732,8 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
|||||||
if not (share and self.detail):
|
if not (share and self.detail):
|
||||||
self.queryset = self.queryset.filter(space=self.request.space)
|
self.queryset = self.queryset.filter(space=self.request.space)
|
||||||
|
|
||||||
params = {x: self.request.GET.get(x) if len({**self.request.GET}[x]) == 1 else self.request.GET.getlist(x) for x in list(self.request.GET)}
|
params = {x: self.request.GET.get(x) if len({**self.request.GET}[x]) == 1 else self.request.GET.getlist(x) for x
|
||||||
|
in list(self.request.GET)}
|
||||||
search = RecipeSearch(self.request, **params)
|
search = RecipeSearch(self.request, **params)
|
||||||
self.queryset = search.get_queryset(self.queryset).prefetch_related('cooklog_set')
|
self.queryset = search.get_queryset(self.queryset).prefetch_related('cooklog_set')
|
||||||
return self.queryset
|
return self.queryset
|
||||||
@ -770,7 +831,8 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
|||||||
levels = int(request.query_params.get('levels', 1))
|
levels = int(request.query_params.get('levels', 1))
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
levels = 1
|
levels = 1
|
||||||
qs = obj.get_related_recipes(levels=levels) # TODO: make levels a user setting, included in request data?, keep solely in the backend?
|
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)
|
||||||
|
|
||||||
|
|
||||||
@ -780,7 +842,8 @@ class ShoppingListRecipeViewSet(viewsets.ModelViewSet):
|
|||||||
permission_classes = [CustomIsOwner | CustomIsShared]
|
permission_classes = [CustomIsOwner | CustomIsShared]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
self.queryset = self.queryset.filter(Q(shoppinglist__space=self.request.space) | Q(entries__space=self.request.space))
|
self.queryset = self.queryset.filter(
|
||||||
|
Q(shoppinglist__space=self.request.space) | Q(entries__space=self.request.space))
|
||||||
return self.queryset.filter(
|
return self.queryset.filter(
|
||||||
Q(shoppinglist__created_by=self.request.user)
|
Q(shoppinglist__created_by=self.request.user)
|
||||||
| Q(shoppinglist__shared=self.request.user)
|
| Q(shoppinglist__shared=self.request.user)
|
||||||
@ -794,12 +857,17 @@ class ShoppingListEntryViewSet(viewsets.ModelViewSet):
|
|||||||
serializer_class = ShoppingListEntrySerializer
|
serializer_class = ShoppingListEntrySerializer
|
||||||
permission_classes = [CustomIsOwner | CustomIsShared]
|
permission_classes = [CustomIsOwner | CustomIsShared]
|
||||||
query_params = [
|
query_params = [
|
||||||
QueryParam(name='id', description=_('Returns the shopping list entry with a primary key of id. Multiple values allowed.'), qtype='int'),
|
QueryParam(name='id',
|
||||||
|
description=_('Returns the shopping list entry with a primary key of id. Multiple values allowed.'),
|
||||||
|
qtype='int'),
|
||||||
QueryParam(
|
QueryParam(
|
||||||
name='checked',
|
name='checked',
|
||||||
description=_('Filter shopping list entries on checked. [''true'', ''false'', ''both'', ''<b>recent</b>'']<br> - ''recent'' includes unchecked items and recently completed items.')
|
description=_(
|
||||||
|
'Filter shopping list entries on checked. [''true'', ''false'', ''both'', ''<b>recent</b>'']<br> - ''recent'' includes unchecked items and recently completed items.')
|
||||||
),
|
),
|
||||||
QueryParam(name='supermarket', description=_('Returns the shopping list entries sorted by supermarket category order.'), qtype='int'),
|
QueryParam(name='supermarket',
|
||||||
|
description=_('Returns the shopping list entries sorted by supermarket category order.'),
|
||||||
|
qtype='int'),
|
||||||
]
|
]
|
||||||
schema = QueryParamAutoSchema()
|
schema = QueryParamAutoSchema()
|
||||||
|
|
||||||
@ -926,6 +994,7 @@ class CustomFilterViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
|||||||
space=self.request.space).distinct()
|
space=self.request.space).distinct()
|
||||||
return super().get_queryset()
|
return super().get_queryset()
|
||||||
|
|
||||||
|
|
||||||
# -------------- non django rest api views --------------------
|
# -------------- non django rest api views --------------------
|
||||||
|
|
||||||
|
|
||||||
|
@ -61,7 +61,8 @@ def search(request):
|
|||||||
if request.user.userpreference.search_style == UserPreference.NEW:
|
if request.user.userpreference.search_style == UserPreference.NEW:
|
||||||
return search_v2(request)
|
return search_v2(request)
|
||||||
f = RecipeFilter(request.GET,
|
f = RecipeFilter(request.GET,
|
||||||
queryset=Recipe.objects.filter(space=request.user.userpreference.space).all().order_by(Lower('name').asc()),
|
queryset=Recipe.objects.filter(space=request.user.userpreference.space).all().order_by(
|
||||||
|
Lower('name').asc()),
|
||||||
space=request.space)
|
space=request.space)
|
||||||
if request.user.userpreference.search_style == UserPreference.LARGE:
|
if request.user.userpreference.search_style == UserPreference.LARGE:
|
||||||
table = RecipeTable(f.qs)
|
table = RecipeTable(f.qs)
|
||||||
@ -225,6 +226,11 @@ def supermarket(request):
|
|||||||
return render(request, 'supermarket.html', {})
|
return render(request, 'supermarket.html', {})
|
||||||
|
|
||||||
|
|
||||||
|
@group_required('user')
|
||||||
|
def ingredient_editor(request):
|
||||||
|
return render(request, 'ingredient_editor.html', {})
|
||||||
|
|
||||||
|
|
||||||
@group_required('user')
|
@group_required('user')
|
||||||
def meal_plan_entry(request, pk):
|
def meal_plan_entry(request, pk):
|
||||||
plan = MealPlan.objects.filter(space=request.space).get(pk=pk)
|
plan = MealPlan.objects.filter(space=request.space).get(pk=pk)
|
||||||
|
118
vue/src/apps/IngredientEditorView/IngredientEditorView.vue
Normal file
118
vue/src/apps/IngredientEditorView/IngredientEditorView.vue
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<generic-multiselect @change="food = $event.val; refreshList()"
|
||||||
|
:model="Models.FOOD"
|
||||||
|
:multiple="false"></generic-multiselect>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<generic-multiselect @change="unit = $event.val; refreshList()"
|
||||||
|
:model="Models.UNIT"
|
||||||
|
:multiple="false"></generic-multiselect>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="table table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Amount</th>
|
||||||
|
<th>Unit</th>
|
||||||
|
<th>Food</th>
|
||||||
|
<th>Note</th>
|
||||||
|
<th>Save</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tr v-for="i in ingredients" v-bind:key="i.id">
|
||||||
|
<td style="width: 10vw">
|
||||||
|
<input type="number" class="form-control" v-model="i.amount">
|
||||||
|
</td>
|
||||||
|
<td style="width: 30vw">
|
||||||
|
<generic-multiselect @change="i.unit = $event.val;"
|
||||||
|
:initial_selection="i.unit"
|
||||||
|
:model="Models.UNIT"
|
||||||
|
:search_on_load="false"
|
||||||
|
:multiple="false"></generic-multiselect>
|
||||||
|
</td>
|
||||||
|
<td style="width: 30vw">
|
||||||
|
<generic-multiselect @change="i.food = $event.val;"
|
||||||
|
:initial_selection="i.food"
|
||||||
|
:model="Models.FOOD"
|
||||||
|
:search_on_load="false"
|
||||||
|
:multiple="false"></generic-multiselect>
|
||||||
|
</td>
|
||||||
|
<td style="width: 30vw">
|
||||||
|
<input class="form-control" v-model="i.note">
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<b-button variant="primary" @click="updateIngredient(i)">Save</b-button>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Vue from "vue"
|
||||||
|
import {BootstrapVue} from "bootstrap-vue"
|
||||||
|
|
||||||
|
import "bootstrap-vue/dist/bootstrap-vue.css"
|
||||||
|
import {ApiMixin, StandardToasts} from "@/utils/utils"
|
||||||
|
import {ApiApiFactory} from "@/utils/openapi/api";
|
||||||
|
import GenericMultiselect from "@/components/GenericMultiselect";
|
||||||
|
|
||||||
|
Vue.use(BootstrapVue)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "IngredientEditorView",
|
||||||
|
mixins: [ApiMixin],
|
||||||
|
components: {GenericMultiselect},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
ingredients: [],
|
||||||
|
food: null,
|
||||||
|
unit: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
mounted() {
|
||||||
|
this.$i18n.locale = window.CUSTOM_LOCALE
|
||||||
|
this.refreshList()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
refreshList: function () {
|
||||||
|
if (this.food === null && this.unit === null) {
|
||||||
|
this.ingredients = []
|
||||||
|
} else {
|
||||||
|
let apiClient = new ApiApiFactory()
|
||||||
|
let params = {'query': {'simple': 1}}
|
||||||
|
if (this.food !== null) {
|
||||||
|
params.query.food = this.food.id
|
||||||
|
}
|
||||||
|
if (this.unit !== null) {
|
||||||
|
params.query.unit = this.unit.id
|
||||||
|
}
|
||||||
|
apiClient.listIngredients(params).then(result => {
|
||||||
|
this.ingredients = result.data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateIngredient: function (i) {
|
||||||
|
let apiClient = new ApiApiFactory()
|
||||||
|
apiClient.updateIngredient(i.id, i).then(r => {
|
||||||
|
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE)
|
||||||
|
}).catch((r, e) => {
|
||||||
|
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
18
vue/src/apps/IngredientEditorView/main.js
Normal file
18
vue/src/apps/IngredientEditorView/main.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import App from './IngredientEditorView.vue'
|
||||||
|
import i18n from '@/i18n'
|
||||||
|
|
||||||
|
Vue.config.productionTip = false
|
||||||
|
|
||||||
|
// TODO move this and other default stuff to centralized JS file (verify nothing breaks)
|
||||||
|
let publicPath = localStorage.STATIC_URL + 'vue/'
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
publicPath = 'http://localhost:8080/'
|
||||||
|
}
|
||||||
|
export default __webpack_public_path__ = publicPath // eslint-disable-line
|
||||||
|
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
i18n,
|
||||||
|
render: h => h(App),
|
||||||
|
}).$mount('#app')
|
@ -68,6 +68,7 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
|
search_on_load: {type: Boolean, default: true},
|
||||||
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"},
|
||||||
@ -123,7 +124,9 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.id = Math.random()
|
this.id = Math.random()
|
||||||
|
if (this.search_on_load) {
|
||||||
this.search("")
|
this.search("")
|
||||||
|
}
|
||||||
if (this.multiple || !this.initial_single_selection) {
|
if (this.multiple || !this.initial_single_selection) {
|
||||||
this.selected_objects = this.initial_selection
|
this.selected_objects = this.initial_selection
|
||||||
} else {
|
} else {
|
||||||
|
@ -472,7 +472,7 @@ export interface FoodRecipe {
|
|||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof FoodRecipe
|
* @memberof FoodRecipe
|
||||||
*/
|
*/
|
||||||
name?: string;
|
name: string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@ -537,7 +537,7 @@ export interface FoodSubstitute {
|
|||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof FoodSubstitute
|
* @memberof FoodSubstitute
|
||||||
*/
|
*/
|
||||||
name?: string;
|
name: string;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -746,6 +746,12 @@ export interface Ingredient {
|
|||||||
* @memberof Ingredient
|
* @memberof Ingredient
|
||||||
*/
|
*/
|
||||||
no_amount?: boolean;
|
no_amount?: boolean;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof Ingredient
|
||||||
|
*/
|
||||||
|
original_text?: string | null;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -1905,6 +1911,12 @@ export interface RecipeIngredients {
|
|||||||
* @memberof RecipeIngredients
|
* @memberof RecipeIngredients
|
||||||
*/
|
*/
|
||||||
no_amount?: boolean;
|
no_amount?: boolean;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof RecipeIngredients
|
||||||
|
*/
|
||||||
|
original_text?: string | null;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -2173,7 +2185,7 @@ export interface RecipeSimple {
|
|||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof RecipeSimple
|
* @memberof RecipeSimple
|
||||||
*/
|
*/
|
||||||
name?: string;
|
name: string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
|
@ -45,6 +45,10 @@ const pages = {
|
|||||||
entry: "./src/apps/MealPlanView/main.js",
|
entry: "./src/apps/MealPlanView/main.js",
|
||||||
chunks: ["chunk-vendors"],
|
chunks: ["chunk-vendors"],
|
||||||
},
|
},
|
||||||
|
ingredient_editor_view: {
|
||||||
|
entry: "./src/apps/IngredientEditorView/main.js",
|
||||||
|
chunks: ["chunk-vendors"],
|
||||||
|
},
|
||||||
shopping_list_view: {
|
shopping_list_view: {
|
||||||
entry: "./src/apps/ShoppingListView/main.js",
|
entry: "./src/apps/ShoppingListView/main.js",
|
||||||
chunks: ["chunk-vendors"],
|
chunks: ["chunk-vendors"],
|
||||||
|
Loading…
Reference in New Issue
Block a user