add new unit/food from shopping list

This commit is contained in:
smilerz 2021-10-31 13:33:15 -05:00
parent 6e9d609fe0
commit a217db5822
16 changed files with 1061 additions and 1106 deletions

View File

@ -228,6 +228,7 @@ class StorageForm(forms.ModelForm):
} }
# TODO: Deprecate
class RecipeBookEntryForm(forms.ModelForm): class RecipeBookEntryForm(forms.ModelForm):
prefix = 'bookmark' prefix = 'bookmark'
@ -480,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' 'mealplan_autoinclude_related', 'default_delay', 'filter_to_supermarket', 'shopping_recent_days'
) )
help_texts = { help_texts = {
@ -494,6 +495,7 @@ class ShoppingPreferenceForm(forms.ModelForm):
'mealplan_autoexclude_onhand': _('When adding a meal plan to the shopping list (manually or automatically), exclude ingredients that are on hand.'), 'mealplan_autoexclude_onhand': _('When adding a meal plan to the shopping list (manually or automatically), exclude ingredients that are on hand.'),
'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.'),
} }
labels = { labels = {
'shopping_share': _('Share Shopping List'), 'shopping_share': _('Share Shopping List'),
@ -503,6 +505,7 @@ class ShoppingPreferenceForm(forms.ModelForm):
'mealplan_autoinclude_related': _('Include Related'), 'mealplan_autoinclude_related': _('Include Related'),
'default_delay': _('Default Delay Hours'), 'default_delay': _('Default Delay Hours'),
'filter_to_supermarket': _('Filter to Supermarket'), 'filter_to_supermarket': _('Filter to Supermarket'),
'shopping_recent_days': _('Recent Days')
} }
widgets = { widgets = {

View File

@ -79,9 +79,6 @@ def is_object_shared(user, obj):
# share checks for relevant objects # share checks for relevant objects
if not user.is_authenticated: if not user.is_authenticated:
return False return False
if obj.__class__.__name__ == 'ShoppingListEntry':
# shopping lists are shared all or none and stored in user preferences
return obj.created_by in user.get_shopping_share()
else: else:
return user in obj.get_shared() return user in obj.get_shared()

View File

@ -30,7 +30,6 @@ def search_recipes(request, queryset, params):
search_steps = params.getlist('steps', []) search_steps = params.getlist('steps', [])
search_units = params.get('units', None) search_units = params.get('units', None)
# TODO I think default behavior should be 'AND' which is how most sites operate with facet/filters based on results
search_keywords_or = str2bool(params.get('keywords_or', True)) search_keywords_or = str2bool(params.get('keywords_or', True))
search_foods_or = str2bool(params.get('foods_or', True)) search_foods_or = str2bool(params.get('foods_or', True))
search_books_or = str2bool(params.get('books_or', True)) search_books_or = str2bool(params.get('books_or', True))
@ -202,20 +201,13 @@ def get_facet(qs=None, request=None, use_cache=True, hash_key=None):
""" """
Gets an annotated list from a queryset. Gets an annotated list from a queryset.
:param qs: :param qs:
recipe queryset to build facets from recipe queryset to build facets from
:param request: :param request:
the web request that contains the necessary query parameters the web request that contains the necessary query parameters
:param use_cache: :param use_cache:
will find results in cache, if any, and return them or empty list. will find results in cache, if any, and return them or empty list.
will save the list of recipes IDs in the cache for future processing will save the list of recipes IDs in the cache for future processing
:param hash_key: :param hash_key:
the cache key of the recipe list to process the cache key of the recipe list to process
only evaluated if the use_cache parameter is false only evaluated if the use_cache parameter is false
""" """
@ -290,7 +282,6 @@ def get_facet(qs=None, request=None, use_cache=True, hash_key=None):
foods = Food.objects.filter(ingredient__step__recipe__in=recipe_list, space=request.space).annotate(recipe_count=Count('ingredient')) foods = Food.objects.filter(ingredient__step__recipe__in=recipe_list, space=request.space).annotate(recipe_count=Count('ingredient'))
food_a = annotated_qs(foods, root=True, fill=True) food_a = annotated_qs(foods, root=True, fill=True)
# TODO add rating facet
facets['Keywords'] = fill_annotated_parents(kw_a, keyword_list) facets['Keywords'] = fill_annotated_parents(kw_a, keyword_list)
facets['Foods'] = fill_annotated_parents(food_a, food_list) facets['Foods'] = fill_annotated_parents(food_a, food_list)
# TODO add book facet # TODO add book facet
@ -363,8 +354,6 @@ def annotated_qs(qs, root=False, fill=False):
dirty = False dirty = False
current_node = node_queue[-1] current_node = node_queue[-1]
depth = current_node.get_depth() depth = current_node.get_depth()
# TODO if node is at the wrong depth for some reason this fails
# either create a 'fix node' page, or automatically move the node to the root
parent_id = current_node.parent parent_id = current_node.parent
if root and depth > 1 and parent_id not in nodes_list: if root and depth > 1 and parent_id not in nodes_list:
parent_id = current_node.parent parent_id = current_node.parent

View File

@ -15,14 +15,12 @@ from recipes import settings
def shopping_helper(qs, request): 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
supermarket_order = ['food__supermarket_category__name', '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'
# qs = qs.annotate(supermarket_category=Coalesce(F('food__supermarket_category__name'), Value(_('Undefined'))))
# TODO add supermarket to API - order by category order
if supermarket: if supermarket:
supermarket_categories = SupermarketCategoryRelation.objects.filter(supermarket=supermarket, category=OuterRef('food__supermarket_category')) supermarket_categories = SupermarketCategoryRelation.objects.filter(supermarket=supermarket, category=OuterRef('food__supermarket_category'))
qs = qs.annotate(supermarket_order=Coalesce(Subquery(supermarket_categories.values('order')), Value(9999))) qs = qs.annotate(supermarket_order=Coalesce(Subquery(supermarket_categories.values('order')), Value(9999)))
@ -33,8 +31,7 @@ def shopping_helper(qs, request):
qs = qs.filter(checked=True) qs = qs.filter(checked=True)
elif checked in ['recent']: elif checked in ['recent']:
today_start = timezone.now().replace(hour=0, minute=0, second=0) today_start = timezone.now().replace(hour=0, minute=0, second=0)
# TODO make recent a user setting week_ago = today_start - timedelta(days=user.userpreference.shopping_recent_days)
week_ago = today_start - timedelta(days=7)
qs = qs.filter(Q(checked=False) | Q(completed_at__gte=week_ago)) qs = qs.filter(Q(checked=False) | Q(completed_at__gte=week_ago))
supermarket_order = ['checked'] + supermarket_order supermarket_order = ['checked'] + supermarket_order
@ -51,7 +48,6 @@ def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None
:param ingredients: Ingredients, list of ingredient IDs to include on the shopping list. When not provided all ingredients will be used :param ingredients: Ingredients, list of ingredient IDs to include on the shopping list. When not provided all ingredients will be used
:param append: If False will remove any entries not included with ingredients, when True will append ingredients to the shopping list :param append: If False will remove any entries not included with ingredients, when True will append ingredients to the shopping list
""" """
# TODO cascade to related recipes
r = recipe or getattr(mealplan, 'recipe', None) or getattr(list_recipe, 'recipe', None) r = recipe or getattr(mealplan, 'recipe', None) or getattr(list_recipe, 'recipe', None)
if not r: if not r:
raise ValueError(_("You must supply a recipe or mealplan")) raise ValueError(_("You must supply a recipe or mealplan"))

View File

@ -2,13 +2,15 @@
import annoying.fields import annoying.fields
from django.conf import settings from django.conf import settings
from django.contrib.postgres.indexes import GinIndex from django.contrib.postgres.indexes import GinIndex
from django.contrib.postgres.search import SearchVectorField, SearchVector from django.contrib.postgres.search import SearchVector, SearchVectorField
from django.db import migrations, models from django.db import migrations, models
from django.db.models import deletion from django.db.models import deletion
from django_scopes import scopes_disabled
from django.utils import translation from django.utils import translation
from django_scopes import scopes_disabled
from cookbook.managers import DICTIONARY from cookbook.managers import DICTIONARY
from cookbook.models import Recipe, Step, Index, PermissionModelMixin, nameSearchField, allSearchFields from cookbook.models import (Index, PermissionModelMixin, Recipe, Step, allSearchFields,
nameSearchField)
def set_default_search_vector(apps, schema_editor): def set_default_search_vector(apps, schema_editor):
@ -16,8 +18,6 @@ def set_default_search_vector(apps, schema_editor):
return return
language = DICTIONARY.get(translation.get_language(), 'simple') language = DICTIONARY.get(translation.get_language(), 'simple')
with scopes_disabled(): with scopes_disabled():
# TODO this approach doesn't work terribly well if multiple languages are in use
# I'm also uncertain about forcing unaccent here
Recipe.objects.all().update( Recipe.objects.all().update(
name_search_vector=SearchVector('name__unaccent', weight='A', config=language), name_search_vector=SearchVector('name__unaccent', weight='A', config=language),
desc_search_vector=SearchVector('description__unaccent', weight='B', config=language) desc_search_vector=SearchVector('description__unaccent', weight='B', config=language)

View File

@ -157,5 +157,10 @@ class Migration(migrations.Migration):
name='filter_to_supermarket', name='filter_to_supermarket',
field=models.BooleanField(default=False), field=models.BooleanField(default=False),
), ),
migrations.AddField(
model_name='userpreference',
name='shopping_recent_days',
field=models.PositiveIntegerField(default=7),
),
migrations.RunPython(copy_values_to_sle), migrations.RunPython(copy_values_to_sle),
] ]

View File

@ -330,7 +330,8 @@ class UserPreference(models.Model, PermissionModelMixin):
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)
filter_to_supermarket = models.BooleanField(default=False) filter_to_supermarket = models.BooleanField(default=False)
default_delay = models.IntegerField(default=4) default_delay = models.DecimalField(default=4, max_digits=8, decimal_places=4)
shopping_recent_days = models.PositiveIntegerField(default=7)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE, null=True) space = models.ForeignKey(Space, on_delete=models.CASCADE, null=True)

View File

@ -164,7 +164,7 @@ class UserPreferenceSerializer(serializers.ModelSerializer):
'user', 'theme', 'nav_color', 'default_unit', 'default_page', 'user', 'theme', 'nav_color', 'default_unit', 'default_page',
'search_style', 'show_recent', 'plan_share', 'ingredient_decimals', 'search_style', 'show_recent', 'plan_share', 'ingredient_decimals',
'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'food_ignore_default', 'default_delay', 'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'food_ignore_default', 'default_delay',
'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share' 'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share', 'shopping_recent_days'
) )

View File

@ -24,7 +24,6 @@ def skip_signal(signal_func):
return _decorator return _decorator
# TODO there is probably a way to generalize this
@receiver(post_save, sender=Recipe) @receiver(post_save, sender=Recipe)
@skip_signal @skip_signal
def update_recipe_search_vector(sender, instance=None, created=False, **kwargs): def update_recipe_search_vector(sender, instance=None, created=False, **kwargs):

View File

@ -1,15 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %} {% comment %} TODO: Deprecate {% endcomment %} {% load django_tables2 %} {% load crispy_forms_tags %} {% load static %} {% load i18n %} {% block title
{% load django_tables2 %} %}{% trans "Shopping List" %}{% endblock %} {% block extra_head %} {% include 'include/vue_base.html' %}
{% load crispy_forms_tags %}
{% load static %}
{% load i18n %}
{% block title %}{% trans "Shopping List" %}{% endblock %} <link rel="stylesheet" href="{% static 'css/vue-multiselect-bs4.min.css' %}" />
{% block extra_head %}
{% include 'include/vue_base.html' %}
<link rel="stylesheet" href="{% static 'css/vue-multiselect-bs4.min.css' %}">
<script src="{% static 'js/vue-multiselect.min.js' %}"></script> <script src="{% static 'js/vue-multiselect.min.js' %}"></script>
<script src="{% static 'js/Sortable.min.js' %}"></script> <script src="{% static 'js/Sortable.min.js' %}"></script>
@ -19,6 +11,11 @@
<script src="{% static 'js/js.cookie.min.js' %}"></script> <script src="{% static 'js/js.cookie.min.js' %}"></script>
<link rel="stylesheet" href="{% static 'css/pretty-checkbox.min.css' %}" />
{% endblock %} {% block content %}
<script src="{% static 'js/js.cookie.min.js' %}"></script>
<link rel="stylesheet" href="{% static 'css/pretty-checkbox.min.css' %}"> <link rel="stylesheet" href="{% static 'css/pretty-checkbox.min.css' %}">
{% endblock %} {% endblock %}
@ -32,10 +29,21 @@
<b-form-checkbox switch size="lg" v-model="edit_mode" <b-form-checkbox switch size="lg" v-model="edit_mode"
@change="$forceUpdate()">{% trans 'Edit' %}</b-form-checkbox> @change="$forceUpdate()">{% trans 'Edit' %}</b-form-checkbox>
</div> </div>
<div class="row">
<span class="col col-md-9">
<h2>{% trans 'Shopping List' %}</h2>
</span>
<span class="col-md-3">
<a href="{% url 'view_shopping_new' %}" class="float-right">
<button class="btn btn-outline-secondary shadow-none"><i class="fas fa-star"></i> {% trans 'Try the new shopping list' %}</button>
</a>
</span>
<div class="col col-mdd-3 text-right">
<b-form-checkbox switch size="lg" v-model="edit_mode" @change="$forceUpdate()">{% trans 'Edit' %}</b-form-checkbox>
</div>
</div> </div>
<template v-if="shopping_list !== undefined"> <template v-if="shopping_list !== undefined">
<div class="text-center" v-if="loading"> <div class="text-center" v-if="loading">
{% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %} {% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
<img class="spinner-tandoor" /> <img class="spinner-tandoor" />
@ -46,25 +54,16 @@
<div v-else-if="edit_mode"> <div v-else-if="edit_mode">
<div class="row"> <div class="row">
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header"><i class="fa fa-search"></i> {% trans 'Search' %}</div>
<i class="fa fa-search"></i> {% trans 'Search' %}
</div>
<div class="card-body"> <div class="card-body">
<input type="text" class="form-control" v-model="recipe_query" @keyup="getRecipes" <input type="text" class="form-control" v-model="recipe_query" @keyup="getRecipes" placeholder="{% trans 'Search Recipe' %}" />
placeholder="{% trans 'Search Recipe' %}">
<ul class="list-group" style="margin-top: 8px"> <ul class="list-group" style="margin-top: 8px">
<li class="list-group-item" v-for="x in recipes"> <li class="list-group-item" v-for="x in recipes">
<div class="row flex-row" style="padding-left: 0.5vw; padding-right: 0.5vw"> <div class="row flex-row" style="padding-left: 0.5vw; padding-right: 0.5vw">
<div class="flex-column flex-fill my-auto"><a v-bind:href="getRecipeUrl(x.id)" <div class="flex-column flex-fill my-auto"><a v-bind:href="getRecipeUrl(x.id)" target="_blank" rel="nofollow norefferer">[[x.name]]</a></div>
target="_blank"
rel="nofollow norefferer">[[x.name]]</a>
</div>
<div class="flex-column align-self-end"> <div class="flex-column align-self-end">
<button class="btn btn-outline-primary shadow-none" <button class="btn btn-outline-primary shadow-none" @click="addRecipeToList(x)"><i class="fa fa-plus"></i></button>
@click="addRecipeToList(x)"><i
class="fa fa-plus"></i></button>
</div> </div>
</div> </div>
</li> </li>
@ -75,41 +74,28 @@
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header"><i class="fa fa-shopping-cart"></i> {% trans 'Shopping Recipes' %}</div>
<i class="fa fa-shopping-cart"></i> {% trans 'Shopping Recipes' %}
</div>
<div class="card-body"> <div class="card-body">
<template v-if="shopping_list.recipes.length < 1"> <template v-if="shopping_list.recipes.length < 1"> {% trans 'No recipes selected' %} </template>
{% trans 'No recipes selected' %}
</template>
<template v-else> <template v-else>
<div class="row flex-row my-auto" v-for="x in shopping_list.recipes" <div class="row flex-row my-auto" v-for="x in shopping_list.recipes" style="margin-top: 1vh !important">
style="margin-top: 1vh!important;">
<div class="flex-column align-self-start" style="margin-right: 0.4vw"> <div class="flex-column align-self-start" style="margin-right: 0.4vw">
<button class="btn btn-outline-danger" @click="removeRecipeFromList(x)"><i <button class="btn btn-outline-danger" @click="removeRecipeFromList(x)"><i class="fa fa-trash"></i></button>
class="fa fa-trash"></i></button>
</div> </div>
<div class="flex-grow-1 flex-column my-auto"><a v-bind:href="getRecipeUrl(x.recipe)" <div class="flex-grow-1 flex-column my-auto">
target="_blank" <a v-bind:href="getRecipeUrl(x.recipe)" target="_blank" rel="nofollow norefferer">[[x.recipe_name]]</a>
rel="nofollow norefferer">[[x.recipe_name]]</a>
</div> </div>
<div class="flex-column align-self-end"> <div class="flex-column align-self-end">
<div class="input-group input-group-sm my-auto"> <div class="input-group input-group-sm my-auto">
<div class="input-group-prepend"> <div class="input-group-prepend">
<button class="text-muted btn btn-outline-primary shadow-none" <button class="text-muted btn btn-outline-primary shadow-none" @click="((x.servings - 1) > 0) ? x.servings -= 1 : 1">-</button>
@click="((x.servings - 1) > 0) ? x.servings -= 1 : 1">-
</button>
</div> </div>
<input class="form-control" type="number" v-model="x.servings"> <input class="form-control" type="number" v-model="x.servings" />
<div class="input-group-append"> <div class="input-group-append">
<button class="text-muted btn btn-outline-primary shadow-none" <button class="text-muted btn btn-outline-primary shadow-none" @click="x.servings += 1">+</button>
@click="x.servings += 1">
+
</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
</div> </div>
@ -118,61 +104,57 @@
</div> </div>
<table class="table table-sm" style="margin-top: 1vh"> <table class="table table-sm" style="margin-top: 1vh">
<template v-for="c in display_categories"> <template v-for="c in display_categories">
<thead> <thead>
<tr> <tr>
<th colspan="5">[[c.name]]</th> <th colspan="5">[[c.name]]</th>
</tr> </tr>
</thead> </thead>
<tbody is="draggable" :list="c.entries" tag="tbody" group="people" @sort="sortEntries" <tbody is="draggable" :list="c.entries" tag="tbody" group="people" @sort="sortEntries" @change="dragChanged(c, $event)" handle=".handle">
@change="dragChanged(c, $event)" handle=".handle"> <tr v-for="(element, index) in c.entries" :key="element.id" v-bind:class="{ 'text-muted': element.checked }">
<tr v-for="(element, index) in c.entries" :key="element.id"
v-bind:class="{ 'text-muted': element.checked }">
<td class="handle"><i class="fas fa-sort"></i></td> <td class="handle"><i class="fas fa-sort"></i></td>
<td>[[element.amount.toFixed(2)]]</td> <td>[[element.amount.toFixed(2)]]</td>
<td>[[element.unit.name]]</td> <td>[[element.unit.name]]</td>
<td>[[element.food.name]]</td> <td>[[element.food.name]]</td>
<td> <td>
<button class="btn btn-sm btn-outline-danger" v-if="element.list_recipe === null" <button
@click="shopping_list.entries = shopping_list.entries.filter(item => item.id !== element.id)"> class="btn btn-sm btn-outline-danger"
<i class="fa fa-trash"></i></button> v-if="element.list_recipe === null"
@click="shopping_list.entries = shopping_list.entries.filter(item => item.id !== element.id)"
>
<i class="fa fa-trash"></i>
</button>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</template> </template>
</table> </table>
<div class="row" style="text-align: right"> <div class="row" style="text-align: right">
<div class="col"> <div class="col">
<b-form-checkbox switch v-model="entry_mode_simple" <b-form-checkbox switch v-model="entry_mode_simple" @change="$cookies.set('shopping_entry_mode_simple',!entry_mode_simple, -1)"
@change="$cookies.set('shopping_entry_mode_simple',!entry_mode_simple, -1)">{% trans 'Entry Mode' %}</b-form-checkbox> >{% trans 'Entry Mode' %}</b-form-checkbox
>
</div> </div>
</div> </div>
<div class="row" v-if="entry_mode_simple" style="margin-top: 2vh"> <div class="row" v-if="entry_mode_simple" style="margin-top: 2vh">
<div class="col-12"> <div class="col-12">
<form v-on:submit.prevent="addSimpleEntry()"> <form v-on:submit.prevent="addSimpleEntry()">
<label for="id_simple_entry">{% trans 'Add Entry' %}</label> <label for="id_simple_entry">{% trans 'Add Entry' %}</label>
<div class="input-group"> <div class="input-group">
<input id="id_simple_entry" class="form-control" v-model="simple_entry"> <input id="id_simple_entry" class="form-control" v-model="simple_entry" />
<div class="input-group-append"> <div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" @click="addSimpleEntry()"><i <button class="btn btn-outline-secondary" type="button" @click="addSimpleEntry()"><i class="fa fa-plus"></i></button>
class="fa fa-plus"></i>
</button>
</div> </div>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<div class="row" v-if="!entry_mode_simple" style="margin-top: 2vh"> <div class="row" v-if="!entry_mode_simple" style="margin-top: 2vh">
<div class="col-12 col-lg-3"> <div class="col-12 col-lg-3">
<input id="id_advanced_entry" class="form-control" type="number" placeholder="{% trans 'Amount' %}" <input id="id_advanced_entry" class="form-control" type="number" placeholder="{% trans 'Amount' %}" v-model="new_entry.amount" ref="new_entry_amount" />
v-model="new_entry.amount" ref="new_entry_amount">
</div> </div>
<div class="col-12 col-lg-4"> <div class="col-12 col-lg-4">
<multiselect <multiselect
@ -193,7 +175,8 @@
track-by="name" track-by="name"
:multiple="false" :multiple="false"
:loading="units_loading" :loading="units_loading"
@search-change="searchUnits"> @search-change="searchUnits"
>
</multiselect> </multiselect>
</div> </div>
<div class="col-12 col-lg-4"> <div class="col-12 col-lg-4">
@ -215,13 +198,13 @@
track-by="name" track-by="name"
:multiple="false" :multiple="false"
:loading="foods_loading" :loading="foods_loading"
@search-change="searchFoods"> @search-change="searchFoods"
>
</multiselect> </multiselect>
</div> </div>
<div class="col-12 col-lg-1 my-auto text-right"> <div class="col-12 col-lg-1 my-auto text-right">
<button class="btn btn-success btn-lg" @click="addEntry()"><i class="fa fa-plus"></i> <button class="btn btn-success btn-lg" @click="addEntry()"><i class="fa fa-plus"></i></button>
</button>
</div> </div>
</div> </div>
@ -243,9 +226,9 @@
track-by="id" track-by="id"
:multiple="false" :multiple="false"
:loading="supermarkets_loading" :loading="supermarkets_loading"
@search-change="searchSupermarket"> @search-change="searchSupermarket"
>
</multiselect> </multiselect>
</div> </div>
</div> </div>
@ -267,34 +250,26 @@
track-by="id" track-by="id"
:multiple="true" :multiple="true"
:loading="users_loading" :loading="users_loading"
@search-change="searchUsers"> @search-change="searchUsers"
>
</multiselect> </multiselect>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col" style="text-align: right; margin-top: 1vh"> <div class="col" style="text-align: right; margin-top: 1vh">
<div class="form-group form-check form-group-lg"> <div class="form-group form-check form-group-lg">
<input class="form-check-input" style="zoom:1.3;" type="checkbox" <input class="form-check-input" style="zoom: 1.3" type="checkbox" v-model="shopping_list.finished" id="id_finished" />
v-model="shopping_list.finished" id="id_finished"> <label class="form-check-label" style="zoom: 1.3" for="id_finished"> {% trans 'Finished' %}</label>
<label class="form-check-label" style="zoom:1.3;" </div>
for="id_finished"> {% trans 'Finished' %}</label>
</div>
</div> </div>
</div> </div>
</div> </div>
<div v-else> <div v-else>
{% if request.user.userpreference.shopping_auto_sync > 0 %} {% if request.user.userpreference.shopping_auto_sync > 0 %}
<div class="row" v-if="!onLine"> <div class="row" v-if="!onLine">
<div class="col col-md-12"> <div class="col col-md-12">
<div class="alert alert-warning" role="alert"> <div class="alert alert-warning" role="alert">{% trans 'You are offline, shopping list might not syncronize.' %}</div>
{% trans 'You are offline, shopping list might not syncronize.' %}
</div>
</div> </div>
</div> </div>
{% endif %} {% endif %}
@ -309,13 +284,10 @@
</tr> </tr>
<tr v-for="x in c.entries"> <tr v-for="x in c.entries">
<template v-if="!x.checked"> <template v-if="!x.checked">
<td><input type="checkbox" style="zoom:1.4;" v-model="x.checked" <td><input type="checkbox" style="zoom: 1.4" v-model="x.checked" @change="entryChecked(x)" /></td>
@change="entryChecked(x)">
</td>
<td>[[x.amount.toFixed(2)]]</td> <td>[[x.amount.toFixed(2)]]</td>
<td>[[x.unit.name]]</td> <td>[[x.unit.name]]</td>
<td>[[x.food.name]] <span class="text-muted" v-if="x.recipes.length > 0">([[x.recipes.join(', ')]])</span> <td>[[x.food.name]] <span class="text-muted" v-if="x.recipes.length > 0">([[x.recipes.join(', ')]])</span></td>
</td>
</template> </template>
</tr> </tr>
</template> </template>
@ -324,34 +296,24 @@
<td colspan="4"></td> <td colspan="4"></td>
</tr> </tr>
<template v-for="c in display_categories"> <template v-for="c in display_categories">
<tr v-for="x in c.entries" class="text-muted"> <tr v-for="x in c.entries" class="text-muted">
<template v-if="x.checked"> <template v-if="x.checked">
<td><input type="checkbox" style="zoom:1.4;" v-model="x.checked" <td><input type="checkbox" style="zoom: 1.4" v-model="x.checked" @change="entryChecked(x)" /></td>
@change="entryChecked(x)">
</td>
<td>[[x.amount]]</td> <td>[[x.amount]]</td>
<td>[[x.unit.name]]</td> <td>[[x.unit.name]]</td>
<td>[[x.food.name]]</td> <td>[[x.food.name]]</td>
</template> </template>
</tr> </tr>
</template> </template>
</table> </table>
</div> </div>
</div> </div>
</div> </div>
<div class="row" style="margin-top: 2vh"> <div class="row" style="margin-top: 2vh">
<div class="col" style="text-align: right"> <div class="col" style="text-align: right">
<b-button class="btn btn-info" v-b-modal.id_modal_export><i <b-button class="btn btn-info" v-b-modal.id_modal_export><i class="fas fa-file-export"></i> {% trans 'Export' %}</b-button>
class="fas fa-file-export"></i> {% trans 'Export' %}</b-button> <button class="btn btn-success" @click="updateShoppingList()" v-if="edit_mode"><i class="fas fa-save"></i> {% trans 'Save' %}</button>
<button class="btn btn-success" @click="updateShoppingList()" v-if="edit_mode"><i
class="fas fa-save"></i> {% trans 'Save' %}
</button>
</div> </div>
</div> </div>
@ -363,28 +325,20 @@
<div class="col col-12"> <div class="col col-12">
<label> <label>
{% trans 'List Prefix' %} {% trans 'List Prefix' %}
<input class="form-control" v-model="export_text_prefix"> <input class="form-control" v-model="export_text_prefix" />
</label> </label>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col col-12"> <div class="col col-12">
<b-form-textarea class="form-control" max-rows="8" v-model="export_text"> <b-form-textarea class="form-control" max-rows="8" v-model="export_text"> </b-form-textarea>
</b-form-textarea>
</div> </div>
</div> </div>
</b-modal> </b-modal>
</template> </template>
{% endblock %} {% block script %}
{% endblock %}
{% block script %}
<script src="{% url 'javascript-catalog' %}"></script> <script src="{% url 'javascript-catalog' %}"></script>
<script type="application/javascript"> <script type="application/javascript">
@ -613,7 +567,6 @@
this.onLine = type === 'online'; this.onLine = type === 'online';
}, },
makeToast: function (title, message, variant = null) { makeToast: function (title, message, variant = null) {
//TODO remove duplicate function in favor of central one
this.$bvToast.toast(message, { this.$bvToast.toast(message, {
title: title, title: title,
variant: variant, variant: variant,
@ -622,7 +575,7 @@
}) })
}, },
loadInitialRecipe: function (recipe, servings) { loadInitialRecipe: function (recipe, servings) {
servings = 1 //TODO temporary until i can actually fix the servings for this #453 servings = 1
return this.$http.get('{% url 'api:recipe-detail' 123456 %}'.replace('123456', recipe)).then((response) => { return this.$http.get('{% url 'api:recipe-detail' 123456 %}'.replace('123456', recipe)).then((response) => {
this.addRecipeToList(response.data, servings) this.addRecipeToList(response.data, servings)
}).catch((err) => { }).catch((err) => {
@ -735,7 +688,7 @@
}) })
}, },
sortEntries: function (a, b) { sortEntries: function (a, b) {
//TODO implement me (might be difficult because of computed drag changed stuff)
}, },
dragChanged: function (category, evt) { dragChanged: function (category, evt) {
if (evt.added !== undefined) { if (evt.added !== undefined) {
@ -871,7 +824,7 @@
this.makeToast(gettext('Error'), gettext("There was an error loading a resource!") + err.bodyText, 'danger') this.makeToast(gettext('Error'), gettext("There was an error loading a resource!") + err.bodyText, 'danger')
}) })
}, },
getRecipeUrl: function (id) { //TODO generic function that can be reused else were getRecipeUrl: function (id) {
return '{% url 'view_recipe' 123456 %}'.replace('123456', id) return '{% url 'view_recipe' 123456 %}'.replace('123456', id)
}, },
addRecipeToList: function (recipe, servings = 1) { addRecipeToList: function (recipe, servings = 1) {
@ -917,7 +870,7 @@
}) })
}, },
searchUnits: function (query) { //TODO move to central component searchUnits: function (query) {
this.units_loading = true this.units_loading = true
this.$http.get("{% url 'api:unit-list' %}" + '?query=' + query + '&limit=10').then((response) => { this.$http.get("{% url 'api:unit-list' %}" + '?query=' + query + '&limit=10').then((response) => {
this.units = response.data.results; this.units = response.data.results;
@ -926,7 +879,7 @@
this.makeToast(gettext('Error'), gettext("There was an error loading a resource!") + err.bodyText, 'danger') this.makeToast(gettext('Error'), gettext("There was an error loading a resource!") + err.bodyText, 'danger')
}) })
}, },
searchFoods: function (query) { //TODO move to central component searchFoods: function (query) {
this.foods_loading = true this.foods_loading = true
this.$http.get("{% url 'api:food-list' %}" + '?query=' + query + '&limit=10').then((response) => { this.$http.get("{% url 'api:food-list' %}" + '?query=' + query + '&limit=10').then((response) => {
this.foods = response.data.results this.foods = response.data.results
@ -935,17 +888,17 @@
this.makeToast(gettext('Error'), gettext("There was an error loading a resource!") + err.bodyText, 'danger') this.makeToast(gettext('Error'), gettext("There was an error loading a resource!") + err.bodyText, 'danger')
}) })
}, },
addFoodType: function (tag, index) { //TODO move to central component addFoodType: function (tag, index) {
let new_food = {'name': tag, supermarket_category: null} let new_food = {'name': tag, supermarket_category: null}
this.foods.push(new_food) this.foods.push(new_food)
this.new_entry.food = new_food this.new_entry.food = new_food
}, },
addUnitType: function (tag, index) { //TODO move to central component addUnitType: function (tag, index) {
let new_unit = {'name': tag} let new_unit = {'name': tag}
this.units.push(new_unit) this.units.push(new_unit)
this.new_entry.unit = new_unit this.new_entry.unit = new_unit
}, },
searchUsers: function (query) { //TODO move to central component searchUsers: function (query) {
this.users_loading = true this.users_loading = true
this.$http.get("{% url 'api:username-list' %}" + '?query=' + query + '&limit=10').then((response) => { this.$http.get("{% url 'api:username-list' %}" + '?query=' + query + '&limit=10').then((response) => {
this.users = response.data this.users = response.data

View File

@ -387,6 +387,7 @@ def user_settings(request):
up.shopping_auto_sync = shopping_form.cleaned_data['shopping_auto_sync'] up.shopping_auto_sync = shopping_form.cleaned_data['shopping_auto_sync']
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']
if up.shopping_auto_sync < settings.SHOPPING_MIN_AUTOSYNC_INTERVAL: if up.shopping_auto_sync < settings.SHOPPING_MIN_AUTOSYNC_INTERVAL:
up.shopping_auto_sync = settings.SHOPPING_MIN_AUTOSYNC_INTERVAL up.shopping_auto_sync = settings.SHOPPING_MIN_AUTOSYNC_INTERVAL
up.save() up.save()

View File

@ -25,24 +25,10 @@
<b-form-input min="1" type="number" :description="$t('Amount')" v-model="new_item.amount"></b-form-input> <b-form-input min="1" type="number" :description="$t('Amount')" v-model="new_item.amount"></b-form-input>
</div> </div>
<div class="col col-md-3"> <div class="col col-md-3">
<generic-multiselect <lookup-input :form="formUnit" :model="Models.UNIT" @change="new_item.unit = $event" :show_label="false" />
@change="new_item.unit = $event.val"
:model="Models.UNIT"
:multiple="false"
:allow_create="false"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
:placeholder="$t('Unit')"
/>
</div> </div>
<div class="col col-md-4"> <div class="col col-md-4">
<generic-multiselect <lookup-input :form="formFood" :model="Models.FOOD" @change="new_item.food = $event" :show_label="false" />
@change="new_item.food = $event.val"
:model="Models.FOOD"
:multiple="false"
:allow_create="false"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
:placeholder="$t('Food')"
/>
</div> </div>
<div class="col col-md-1 "> <div class="col col-md-1 ">
<b-button variant="link" class="px-0"> <b-button variant="link" class="px-0">
@ -107,7 +93,7 @@
</tr> </tr>
</table> </table>
</b-tab> </b-tab>
<!-- settings tab --> <!-- supermarkets tab -->
<b-tab :title="$t('Supermarkets')"> <b-tab :title="$t('Supermarkets')">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col col-md-5"> <div class="col col-md-5">
@ -183,9 +169,7 @@
</div> </div>
</b-card> </b-card>
<b-card-sub-title v-if="new_supermarket.editmode" class="pt-0 pb-3" <b-card-sub-title v-if="new_supermarket.editmode" class="pt-0 pb-3">{{ $t("CategoryInstruction") }}</b-card-sub-title>
>Drag categories to change the order categories appear in shopping list.</b-card-sub-title
>
<b-card <b-card
v-if="new_supermarket.editmode && supermarketCategory.length === 0" v-if="new_supermarket.editmode && supermarketCategory.length === 0"
class="m-0 p-0 font-weight-bold no-body" class="m-0 p-0 font-weight-bold no-body"
@ -328,6 +312,20 @@
</em> </em>
</div> </div>
</div> </div>
<div class="row">
<div class="col col-md-6">{{ $t("shopping_recent_days") }}</div>
<div class="col col-md-6 text-right">
<input type="number" size="sm" v-model="settings.shopping_recent_days" @change="saveSettings" />
</div>
</div>
<div class="row sm mb-3">
<div class="col">
<em class="small text-muted">
{{ $t("shopping_recent_days_desc") }}
</em>
</div>
</div>
<div class="row"> <div class="row">
<div class="col col-md-6">{{ $t("filter_to_supermarket") }}</div> <div class="col col-md-6">{{ $t("filter_to_supermarket") }}</div>
<div class="col col-md-6 text-right"> <div class="col col-md-6 text-right">
@ -459,6 +457,7 @@ import ContextMenuItem from "@/components/ContextMenu/ContextMenuItem"
import ShoppingLineItem from "@/components/ShoppingLineItem" import ShoppingLineItem from "@/components/ShoppingLineItem"
import GenericMultiselect from "@/components/GenericMultiselect" import GenericMultiselect from "@/components/GenericMultiselect"
import GenericPill from "@/components/GenericPill" import GenericPill from "@/components/GenericPill"
import LookupInput from "@/components/Modals/LookupInput"
import draggable from "vuedraggable" import draggable from "vuedraggable"
import { ApiMixin, getUserPreference } from "@/utils/utils" import { ApiMixin, getUserPreference } from "@/utils/utils"
@ -470,7 +469,7 @@ Vue.use(BootstrapVue)
export default { export default {
name: "ShoppingListView", name: "ShoppingListView",
mixins: [ApiMixin], mixins: [ApiMixin],
components: { ContextMenu, ContextMenuItem, ShoppingLineItem, GenericMultiselect, GenericPill, draggable }, components: { ContextMenu, ContextMenuItem, ShoppingLineItem, GenericMultiselect, GenericPill, draggable, LookupInput },
data() { data() {
return { return {
@ -492,6 +491,7 @@ export default {
mealplan_autoinclude_related: false, mealplan_autoinclude_related: false,
mealplan_autoexclude_onhand: true, mealplan_autoexclude_onhand: true,
filter_to_supermarket: false, filter_to_supermarket: false,
shopping_recent_days: 7,
}, },
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 },
@ -577,6 +577,16 @@ export default {
defaultDelay() { defaultDelay() {
return getUserPreference("default_delay") || 2 return getUserPreference("default_delay") || 2
}, },
formUnit() {
let unit = this.Models.SHOPPING_LIST.create.form.unit
unit.value = this.new_item.unit
return unit
},
formFood() {
let food = this.Models.SHOPPING_LIST.create.form.food
food.value = this.new_item.food
return food
},
itemsDelayed() { itemsDelayed() {
return this.items.filter((x) => !x.delay_until || !Date.parse(x?.delay_until) > new Date(Date.now())).length < this.items.length return this.items.filter((x) => !x.delay_until || !Date.parse(x?.delay_until) > new Date(Date.now())).length < this.items.length
}, },
@ -647,6 +657,10 @@ export default {
}, },
methods: { methods: {
// this.genericAPI inherited from ApiMixin // this.genericAPI inherited from ApiMixin
test(e) {
this.new_item.unit = e
console.log(e, this.new_item, this.formUnit)
},
addItem() { addItem() {
let api = new ApiApiFactory() let api = new ApiApiFactory()
api.createShoppingListEntry(this.new_item) api.createShoppingListEntry(this.new_item)

View File

@ -1,53 +1,40 @@
<template> <template>
<!-- TODO: Deprecate -->
<div id="app"> <div id="app">
<div class="row"> <div class="row">
<div class="col col-md-12"> <div class="col col-md-12">
<h2>{{ $t('Supermarket') }}</h2> <h2>{{ $t("Supermarket") }}</h2>
<multiselect v-model="selected_supermarket" track-by="id" label="name" <multiselect v-model="selected_supermarket" track-by="id" label="name" :options="supermarkets" @input="selectedSupermarketChanged"> </multiselect>
:options="supermarkets" @input="selectedSupermarketChanged">
</multiselect>
<b-button class="btn btn-primary btn-block" style="margin-top: 1vh" v-b-modal.modal-supermarket> <b-button class="btn btn-primary btn-block" style="margin-top: 1vh" v-b-modal.modal-supermarket>
{{ $t('Edit') }} {{ $t("Edit") }}
</b-button>
<b-button class="btn btn-success btn-block" @click="selected_supermarket = {new:true, name:''}"
v-b-modal.modal-supermarket>{{ $t('New') }}
</b-button> </b-button>
<b-button class="btn btn-success btn-block" @click="selected_supermarket = { new: true, name: '' }" v-b-modal.modal-supermarket>{{ $t("New") }} </b-button>
</div> </div>
</div> </div>
<hr> <hr />
<div class="row"> <div class="row">
<div class="col col-md-6"> <div class="col col-md-6">
<h4>{{ $t('Categories') }} <h4>
<button class="btn btn-success btn-sm" @click="selected_category = {new:true, name:''}" {{ $t("Categories") }}
v-b-modal.modal-category>{{ $t('New') }} <button class="btn btn-success btn-sm" @click="selected_category = { new: true, name: '' }" v-b-modal.modal-category>{{ $t("New") }}</button>
</button>
</h4> </h4>
<draggable :list="selectable_categories" group="supermarket_categories" <draggable :list="selectable_categories" group="supermarket_categories" :empty-insert-threshold="10">
:empty-insert-threshold="10">
<div v-for="c in selectable_categories" :key="c.id"> <div v-for="c in selectable_categories" :key="c.id">
<button class="btn btn-block btn-sm btn-primary" style="margin-top: 0.5vh">{{ c.name }}</button> <button class="btn btn-block btn-sm btn-primary" style="margin-top: 0.5vh">{{ c.name }}</button>
</div> </div>
</draggable> </draggable>
</div> </div>
<div class="col col-md-6"> <div class="col col-md-6">
<h4>{{ $t('Selected') }} {{ $t('Categories') }}</h4> <h4>{{ $t("Selected") }} {{ $t("Categories") }}</h4>
<draggable :list="supermarket_categories" group="supermarket_categories" <draggable :list="supermarket_categories" group="supermarket_categories" :empty-insert-threshold="10" @change="selectedCategoriesChanged">
:empty-insert-threshold="10" @change="selectedCategoriesChanged">
<div v-for="c in supermarket_categories" :key="c.id"> <div v-for="c in supermarket_categories" :key="c.id">
<button class="btn btn-block btn-sm btn-primary" style="margin-top: 0.5vh">{{ c.name }}</button> <button class="btn btn-block btn-sm btn-primary" style="margin-top: 0.5vh">{{ c.name }}</button>
</div> </div>
</draggable> </draggable>
</div> </div>
@ -56,52 +43,45 @@
<!-- EDIT MODALS --> <!-- EDIT MODALS -->
<b-modal id="modal-supermarket" v-bind:title="$t('Supermarket')" @ok="supermarketModalOk()"> <b-modal id="modal-supermarket" v-bind:title="$t('Supermarket')" @ok="supermarketModalOk()">
<label v-if="selected_supermarket !== undefined"> <label v-if="selected_supermarket !== undefined">
{{ $t('Name') }} {{ $t("Name") }}
<b-input v-model="selected_supermarket.name"></b-input> <b-input v-model="selected_supermarket.name"></b-input>
</label> </label>
</b-modal> </b-modal>
<b-modal id="modal-category" v-bind:title="$t('Category')" @ok="categoryModalOk()"> <b-modal id="modal-category" v-bind:title="$t('Category')" @ok="categoryModalOk()">
<label v-if="selected_category !== undefined"> <label v-if="selected_category !== undefined">
{{ $t('Name') }} {{ $t("Name") }}
<b-input v-model="selected_category.name"></b-input> <b-input v-model="selected_category.name"></b-input>
</label> </label>
</b-modal> </b-modal>
</div> </div>
</template> </template>
<script> <script>
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 {ResolveUrlMixin, ToastMixin} from "@/utils/utils"; import { ResolveUrlMixin, ToastMixin } from "@/utils/utils"
import { ApiApiFactory } from "@/utils/openapi/api.ts"
import {ApiApiFactory} from "@/utils/openapi/api.ts";
Vue.use(BootstrapVue) Vue.use(BootstrapVue)
import draggable from 'vuedraggable' import draggable from "vuedraggable"
import axios from 'axios' import axios from "axios"
import Multiselect from "vue-multiselect"; import Multiselect from "vue-multiselect"
axios.defaults.xsrfHeaderName = 'X-CSRFToken' axios.defaults.xsrfHeaderName = "X-CSRFToken"
axios.defaults.xsrfCookieName = 'csrftoken' axios.defaults.xsrfCookieName = "csrftoken"
export default { export default {
name: 'SupermarketView', name: "SupermarketView",
mixins: [ mixins: [ResolveUrlMixin, ToastMixin],
ResolveUrlMixin,
ToastMixin,
],
components: { components: {
Multiselect, Multiselect,
draggable draggable,
}, },
data() { data() {
return { return {
@ -111,7 +91,6 @@ export default {
selected_supermarket: {}, selected_supermarket: {},
selected_category: {}, selected_category: {},
selectable_categories: [], selectable_categories: [],
supermarket_categories: [], supermarket_categories: [],
} }
@ -123,10 +102,10 @@ export default {
methods: { methods: {
loadInitial: function() { loadInitial: function() {
let apiClient = new ApiApiFactory() let apiClient = new ApiApiFactory()
apiClient.listSupermarkets().then(results => { apiClient.listSupermarkets().then((results) => {
this.supermarkets = results.data this.supermarkets = results.data
}) })
apiClient.listSupermarketCategorys().then(results => { apiClient.listSupermarketCategorys().then((results) => {
this.categories = results.data this.categories = results.data
this.selectable_categories = this.categories this.selectable_categories = this.categories
}) })
@ -134,21 +113,24 @@ export default {
selectedCategoriesChanged: function(data) { selectedCategoriesChanged: function(data) {
let apiClient = new ApiApiFactory() let apiClient = new ApiApiFactory()
if ('removed' in data) { if ("removed" in data) {
let relation = this.selected_supermarket.category_to_supermarket.filter((el) => el.category.id === data.removed.element.id)[0] let relation = this.selected_supermarket.category_to_supermarket.filter((el) => el.category.id === data.removed.element.id)[0]
apiClient.destroySupermarketCategoryRelation(relation.id) apiClient.destroySupermarketCategoryRelation(relation.id)
} }
if ('added' in data) { if ("added" in data) {
apiClient.createSupermarketCategoryRelation({ apiClient
.createSupermarketCategoryRelation({
category: data.added.element, category: data.added.element,
supermarket: this.selected_supermarket.id, order: 0 supermarket: this.selected_supermarket.id,
}).then(results => { order: 0,
})
.then((results) => {
this.selected_supermarket.category_to_supermarket.push(results.data) this.selected_supermarket.category_to_supermarket.push(results.data)
}) })
} }
if ('moved' in data || 'added' in data) { if ("moved" in data || "added" in data) {
this.supermarket_categories.forEach((element, index) => { this.supermarket_categories.forEach((element, index) => {
let relation = this.selected_supermarket.category_to_supermarket.filter((el) => el.category.id === element.id)[0] let relation = this.selected_supermarket.category_to_supermarket.filter((el) => el.category.id === element.id)[0]
console.log(relation) console.log(relation)
@ -164,38 +146,33 @@ export default {
this.supermarket_categories.push(i.category) this.supermarket_categories.push(i.category)
this.selectable_categories = this.selectable_categories.filter(function(el) { this.selectable_categories = this.selectable_categories.filter(function(el) {
return el.id !== i.category.id return el.id !== i.category.id
}); })
} }
}, },
supermarketModalOk: function() { supermarketModalOk: function() {
let apiClient = new ApiApiFactory() let apiClient = new ApiApiFactory()
if (this.selected_supermarket.new) { if (this.selected_supermarket.new) {
apiClient.createSupermarket({name: this.selected_supermarket.name}).then(results => { apiClient.createSupermarket({ name: this.selected_supermarket.name }).then((results) => {
this.selected_supermarket = undefined this.selected_supermarket = undefined
this.loadInitial() this.loadInitial()
}) })
} else { } else {
apiClient.partialUpdateSupermarket(this.selected_supermarket.id, { name: this.selected_supermarket.name }) apiClient.partialUpdateSupermarket(this.selected_supermarket.id, { name: this.selected_supermarket.name })
} }
}, },
categoryModalOk: function() { categoryModalOk: function() {
let apiClient = new ApiApiFactory() let apiClient = new ApiApiFactory()
if (this.selected_category.new) { if (this.selected_category.new) {
apiClient.createSupermarketCategory({name: this.selected_category.name}).then(results => { apiClient.createSupermarketCategory({ name: this.selected_category.name }).then((results) => {
this.selected_category = {} this.selected_category = {}
this.loadInitial() this.loadInitial()
}) })
} else { } else {
apiClient.partialUpdateSupermarketCategory(this.selected_category.id, { name: this.selected_category.name }) apiClient.partialUpdateSupermarketCategory(this.selected_category.id, { name: this.selected_category.name })
} }
},
},
} }
}
}
</script> </script>
<style> <style></style>
</style>

View File

@ -8,7 +8,6 @@
<p v-if="f.type == 'instruction'">{{ f.label }}</p> <p v-if="f.type == 'instruction'">{{ f.label }}</p>
<!-- this lookup is single selection --> <!-- this lookup is single selection -->
<lookup-input v-if="f.type == 'lookup'" :form="f" :model="listModel(f.list)" @change="storeValue" /> <lookup-input v-if="f.type == 'lookup'" :form="f" :model="listModel(f.list)" @change="storeValue" />
<!-- TODO add ability to create new items associated with lookup -->
<!-- TODO: add multi-selection input list --> <!-- TODO: add multi-selection input list -->
<checkbox-input v-if="f.type == 'checkbox'" :label="f.label" :value="f.value" :field="f.field" /> <checkbox-input v-if="f.type == 'checkbox'" :label="f.label" :value="f.value" :field="f.field" />
<text-input v-if="f.type == 'text'" :label="f.label" :value="f.value" :field="f.field" :placeholder="f.placeholder" /> <text-input v-if="f.type == 'text'" :label="f.label" :value="f.value" :field="f.field" :placeholder="f.placeholder" />

View File

@ -238,7 +238,6 @@
"mealplan_autoinclude_related_desc": "When adding a meal plan to the shopping list (manually or automatically), include all related recipes.", "mealplan_autoinclude_related_desc": "When adding a meal plan to the shopping list (manually or automatically), include all related recipes.",
"default_delay_desc": "Default number of hours to delay a shopping list entry.", "default_delay_desc": "Default number of hours to delay a shopping list entry.",
"filter_to_supermarket": "Filter to Supermarket", "filter_to_supermarket": "Filter to Supermarket",
"filter_to_supermarket_desc": "Filter shopping list to only include supermarket categories.",
"Week_Numbers": "Week numbers", "Week_Numbers": "Week numbers",
"Show_Week_Numbers": "Show week numbers ?", "Show_Week_Numbers": "Show week numbers ?",
"Export_As_ICal": "Export current period to iCal format", "Export_As_ICal": "Export current period to iCal format",
@ -260,6 +259,10 @@
"nothing": "Nothing to do", "nothing": "Nothing to do",
"err_merge_self": "Cannot merge item with itself", "err_merge_self": "Cannot merge item with itself",
"show_sql": "Show SQL", "show_sql": "Show SQL",
"filter_to_supermarket_desc": "By default, filter shopping list to only include categories for selected supermarket.",
"CategoryName": "Category Name", "CategoryName": "Category Name",
"SupermarketName": "Supermarket Name" "SupermarketName": "Supermarket Name",
"CategoryInstruction": "Drag categories to change the order categories appear in shopping list.",
"shopping_recent_days_desc": "Days of recent shopping list entries to display.",
"shopping_recent_days": "Recent Days"
} }

View File

@ -216,6 +216,24 @@ export class Models {
}, },
create: { create: {
params: [["amount", "unit", "food", "checked"]], params: [["amount", "unit", "food", "checked"]],
form: {
unit: {
form_field: true,
type: "lookup",
field: "unit",
list: "UNIT",
label: i18n.t("Unit"),
allow_create: true,
},
food: {
form_field: true,
type: "lookup",
field: "food",
list: "FOOD",
label: i18n.t("Food"),
allow_create: true,
},
},
}, },
} }