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):

File diff suppressed because it is too large Load Diff

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,201 +1,178 @@
<template> <template>
<!-- TODO: Deprecate -->
<div id="app">
<div class="row">
<div class="col col-md-12">
<h2>{{ $t("Supermarket") }}</h2>
<div id="app"> <multiselect v-model="selected_supermarket" track-by="id" label="name" :options="supermarkets" @input="selectedSupermarketChanged"> </multiselect>
<div class="row"> <b-button class="btn btn-primary btn-block" style="margin-top: 1vh" v-b-modal.modal-supermarket>
{{ $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>
</div>
</div>
<div class="col col-md-12"> <hr />
<h2>{{ $t('Supermarket') }}</h2>
<multiselect v-model="selected_supermarket" track-by="id" label="name" <div class="row">
:options="supermarkets" @input="selectedSupermarketChanged"> <div class="col col-md-6">
</multiselect> <h4>
{{ $t("Categories") }}
<button class="btn btn-success btn-sm" @click="selected_category = { new: true, name: '' }" v-b-modal.modal-category>{{ $t("New") }}</button>
</h4>
<b-button class="btn btn-primary btn-block" style="margin-top: 1vh" v-b-modal.modal-supermarket> <draggable :list="selectable_categories" group="supermarket_categories" :empty-insert-threshold="10">
{{ $t('Edit') }} <div v-for="c in selectable_categories" :key="c.id">
</b-button> <button class="btn btn-block btn-sm btn-primary" style="margin-top: 0.5vh">{{ c.name }}</button>
<b-button class="btn btn-success btn-block" @click="selected_supermarket = {new:true, name:''}" </div>
v-b-modal.modal-supermarket>{{ $t('New') }} </draggable>
</b-button> </div>
</div> <div class="col col-md-6">
<h4>{{ $t("Selected") }} {{ $t("Categories") }}</h4>
<draggable :list="supermarket_categories" group="supermarket_categories" :empty-insert-threshold="10" @change="selectedCategoriesChanged">
<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>
</div>
</draggable>
</div>
</div>
<!-- EDIT MODALS -->
<b-modal id="modal-supermarket" v-bind:title="$t('Supermarket')" @ok="supermarketModalOk()">
<label v-if="selected_supermarket !== undefined">
{{ $t("Name") }}
<b-input v-model="selected_supermarket.name"></b-input>
</label>
</b-modal>
<b-modal id="modal-category" v-bind:title="$t('Category')" @ok="categoryModalOk()">
<label v-if="selected_category !== undefined">
{{ $t("Name") }}
<b-input v-model="selected_category.name"></b-input>
</label>
</b-modal>
</div> </div>
<hr>
<div class="row">
<div class="col col-md-6">
<h4>{{ $t('Categories') }}
<button class="btn btn-success btn-sm" @click="selected_category = {new:true, name:''}"
v-b-modal.modal-category>{{ $t('New') }}
</button>
</h4>
<draggable :list="selectable_categories" group="supermarket_categories"
:empty-insert-threshold="10">
<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>
</div>
</draggable>
</div>
<div class="col col-md-6">
<h4>{{ $t('Selected') }} {{ $t('Categories') }}</h4>
<draggable :list="supermarket_categories" group="supermarket_categories"
:empty-insert-threshold="10" @change="selectedCategoriesChanged">
<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>
</div>
</draggable>
</div>
</div>
<!-- EDIT MODALS -->
<b-modal id="modal-supermarket" v-bind:title="$t('Supermarket')" @ok="supermarketModalOk()">
<label v-if="selected_supermarket !== undefined">
{{ $t('Name') }}
<b-input v-model="selected_supermarket.name"></b-input>
</label>
</b-modal>
<b-modal id="modal-category" v-bind:title="$t('Category')" @ok="categoryModalOk()">
<label v-if="selected_category !== undefined">
{{ $t('Name') }}
<b-input v-model="selected_category.name"></b-input>
</label>
</b-modal>
</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, components: {
ToastMixin, Multiselect,
], draggable,
components: {
Multiselect,
draggable
},
data() {
return {
supermarkets: [],
categories: [],
selected_supermarket: {},
selected_category: {},
selectable_categories: [],
supermarket_categories: [],
}
},
mounted() {
this.$i18n.locale = window.CUSTOM_LOCALE
this.loadInitial()
},
methods: {
loadInitial: function () {
let apiClient = new ApiApiFactory()
apiClient.listSupermarkets().then(results => {
this.supermarkets = results.data
})
apiClient.listSupermarketCategorys().then(results => {
this.categories = results.data
this.selectable_categories = this.categories
})
}, },
selectedCategoriesChanged: function (data) { data() {
let apiClient = new ApiApiFactory() return {
supermarkets: [],
categories: [],
if ('removed' in data) { selected_supermarket: {},
let relation = this.selected_supermarket.category_to_supermarket.filter((el) => el.category.id === data.removed.element.id)[0] selected_category: {},
apiClient.destroySupermarketCategoryRelation(relation.id)
}
if ('added' in data) { selectable_categories: [],
apiClient.createSupermarketCategoryRelation({ supermarket_categories: [],
category: data.added.element, }
supermarket: this.selected_supermarket.id, order: 0
}).then(results => {
this.selected_supermarket.category_to_supermarket.push(results.data)
})
}
if ('moved' in data || 'added' in data) {
this.supermarket_categories.forEach( (element,index) =>{
let relation = this.selected_supermarket.category_to_supermarket.filter((el) => el.category.id === element.id)[0]
console.log(relation)
apiClient.partialUpdateSupermarketCategoryRelation(relation.id, {order: index})
})
}
}, },
selectedSupermarketChanged: function (supermarket, id) { mounted() {
this.supermarket_categories = [] this.$i18n.locale = window.CUSTOM_LOCALE
this.selectable_categories = this.categories this.loadInitial()
for (let i of supermarket.category_to_supermarket) {
this.supermarket_categories.push(i.category)
this.selectable_categories = this.selectable_categories.filter(function (el) {
return el.id !== i.category.id
});
}
}, },
supermarketModalOk: function () { methods: {
let apiClient = new ApiApiFactory() loadInitial: function() {
if (this.selected_supermarket.new) { let apiClient = new ApiApiFactory()
apiClient.createSupermarket({name: this.selected_supermarket.name}).then(results => { apiClient.listSupermarkets().then((results) => {
this.selected_supermarket = undefined this.supermarkets = results.data
this.loadInitial() })
}) apiClient.listSupermarketCategorys().then((results) => {
} else { this.categories = results.data
apiClient.partialUpdateSupermarket(this.selected_supermarket.id, {name: this.selected_supermarket.name}) this.selectable_categories = this.categories
})
},
selectedCategoriesChanged: function(data) {
let apiClient = new ApiApiFactory()
} if ("removed" in data) {
let relation = this.selected_supermarket.category_to_supermarket.filter((el) => el.category.id === data.removed.element.id)[0]
apiClient.destroySupermarketCategoryRelation(relation.id)
}
if ("added" in data) {
apiClient
.createSupermarketCategoryRelation({
category: data.added.element,
supermarket: this.selected_supermarket.id,
order: 0,
})
.then((results) => {
this.selected_supermarket.category_to_supermarket.push(results.data)
})
}
if ("moved" in data || "added" in data) {
this.supermarket_categories.forEach((element, index) => {
let relation = this.selected_supermarket.category_to_supermarket.filter((el) => el.category.id === element.id)[0]
console.log(relation)
apiClient.partialUpdateSupermarketCategoryRelation(relation.id, { order: index })
})
}
},
selectedSupermarketChanged: function(supermarket, id) {
this.supermarket_categories = []
this.selectable_categories = this.categories
for (let i of supermarket.category_to_supermarket) {
this.supermarket_categories.push(i.category)
this.selectable_categories = this.selectable_categories.filter(function(el) {
return el.id !== i.category.id
})
}
},
supermarketModalOk: function() {
let apiClient = new ApiApiFactory()
if (this.selected_supermarket.new) {
apiClient.createSupermarket({ name: this.selected_supermarket.name }).then((results) => {
this.selected_supermarket = undefined
this.loadInitial()
})
} else {
apiClient.partialUpdateSupermarket(this.selected_supermarket.id, { name: this.selected_supermarket.name })
}
},
categoryModalOk: function() {
let apiClient = new ApiApiFactory()
if (this.selected_category.new) {
apiClient.createSupermarketCategory({ name: this.selected_category.name }).then((results) => {
this.selected_category = {}
this.loadInitial()
})
} else {
apiClient.partialUpdateSupermarketCategory(this.selected_category.id, { name: this.selected_category.name })
}
},
}, },
categoryModalOk: function () {
let apiClient = new ApiApiFactory()
if (this.selected_category.new) {
apiClient.createSupermarketCategory({name: this.selected_category.name}).then(results => {
this.selected_category = {}
this.loadInitial()
})
} else {
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,
},
},
}, },
} }