diff --git a/cookbook/forms.py b/cookbook/forms.py
index 9d024c35d..3b208af8f 100644
--- a/cookbook/forms.py
+++ b/cookbook/forms.py
@@ -228,6 +228,7 @@ class StorageForm(forms.ModelForm):
}
+# TODO: Deprecate
class RecipeBookEntryForm(forms.ModelForm):
prefix = 'bookmark'
@@ -480,7 +481,7 @@ class ShoppingPreferenceForm(forms.ModelForm):
fields = (
'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 = {
@@ -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.'),
'default_delay': _('Default number of hours to delay a shopping list entry.'),
'filter_to_supermarket': _('Filter shopping list to only include supermarket categories.'),
+ 'shopping_recent_days': _('Days of recent shopping list entries to display.'),
}
labels = {
'shopping_share': _('Share Shopping List'),
@@ -503,6 +505,7 @@ class ShoppingPreferenceForm(forms.ModelForm):
'mealplan_autoinclude_related': _('Include Related'),
'default_delay': _('Default Delay Hours'),
'filter_to_supermarket': _('Filter to Supermarket'),
+ 'shopping_recent_days': _('Recent Days')
}
widgets = {
diff --git a/cookbook/helper/permission_helper.py b/cookbook/helper/permission_helper.py
index ea6bcdb58..b5230d0d2 100644
--- a/cookbook/helper/permission_helper.py
+++ b/cookbook/helper/permission_helper.py
@@ -79,9 +79,6 @@ def is_object_shared(user, obj):
# share checks for relevant objects
if not user.is_authenticated:
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:
return user in obj.get_shared()
diff --git a/cookbook/helper/recipe_search.py b/cookbook/helper/recipe_search.py
index ce87e0aae..9ad2b81f5 100644
--- a/cookbook/helper/recipe_search.py
+++ b/cookbook/helper/recipe_search.py
@@ -30,7 +30,6 @@ def search_recipes(request, queryset, params):
search_steps = params.getlist('steps', [])
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_foods_or = str2bool(params.get('foods_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.
:param qs:
-
recipe queryset to build facets from
-
:param request:
-
the web request that contains the necessary query parameters
-
:param use_cache:
-
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
-
:param hash_key:
-
the cache key of the recipe list to process
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'))
food_a = annotated_qs(foods, root=True, fill=True)
- # TODO add rating facet
facets['Keywords'] = fill_annotated_parents(kw_a, keyword_list)
facets['Foods'] = fill_annotated_parents(food_a, food_list)
# TODO add book facet
@@ -363,8 +354,6 @@ def annotated_qs(qs, root=False, fill=False):
dirty = False
current_node = node_queue[-1]
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
if root and depth > 1 and parent_id not in nodes_list:
parent_id = current_node.parent
diff --git a/cookbook/helper/shopping_helper.py b/cookbook/helper/shopping_helper.py
index 17f9915b2..e9cfa8a63 100644
--- a/cookbook/helper/shopping_helper.py
+++ b/cookbook/helper/shopping_helper.py
@@ -15,14 +15,12 @@ from recipes import settings
def shopping_helper(qs, request):
supermarket = request.query_params.get('supermarket', None)
checked = request.query_params.get('checked', 'recent')
+ user = request.user
supermarket_order = ['food__supermarket_category__name', 'food__name']
# TODO created either scheduled task or startup task to delete very old shopping list entries
# 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:
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)))
@@ -33,8 +31,7 @@ def shopping_helper(qs, request):
qs = qs.filter(checked=True)
elif checked in ['recent']:
today_start = timezone.now().replace(hour=0, minute=0, second=0)
- # TODO make recent a user setting
- week_ago = today_start - timedelta(days=7)
+ week_ago = today_start - timedelta(days=user.userpreference.shopping_recent_days)
qs = qs.filter(Q(checked=False) | Q(completed_at__gte=week_ago))
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 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)
if not r:
raise ValueError(_("You must supply a recipe or mealplan"))
diff --git a/cookbook/migrations/0143_build_full_text_index.py b/cookbook/migrations/0143_build_full_text_index.py
index ca58fb0e1..927eaa94c 100644
--- a/cookbook/migrations/0143_build_full_text_index.py
+++ b/cookbook/migrations/0143_build_full_text_index.py
@@ -2,13 +2,15 @@
import annoying.fields
from django.conf import settings
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.models import deletion
-from django_scopes import scopes_disabled
from django.utils import translation
+from django_scopes import scopes_disabled
+
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):
@@ -16,8 +18,6 @@ def set_default_search_vector(apps, schema_editor):
return
language = DICTIONARY.get(translation.get_language(), 'simple')
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(
name_search_vector=SearchVector('name__unaccent', weight='A', config=language),
desc_search_vector=SearchVector('description__unaccent', weight='B', config=language)
diff --git a/cookbook/migrations/0159_add_shoppinglistentry_fields.py b/cookbook/migrations/0159_add_shoppinglistentry_fields.py
index a752925ce..df9f4ae36 100644
--- a/cookbook/migrations/0159_add_shoppinglistentry_fields.py
+++ b/cookbook/migrations/0159_add_shoppinglistentry_fields.py
@@ -157,5 +157,10 @@ class Migration(migrations.Migration):
name='filter_to_supermarket',
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),
]
diff --git a/cookbook/models.py b/cookbook/models.py
index 7fbff948e..a00e1e523 100644
--- a/cookbook/models.py
+++ b/cookbook/models.py
@@ -330,7 +330,8 @@ class UserPreference(models.Model, PermissionModelMixin):
mealplan_autoexclude_onhand = models.BooleanField(default=True)
mealplan_autoinclude_related = models.BooleanField(default=True)
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)
space = models.ForeignKey(Space, on_delete=models.CASCADE, null=True)
diff --git a/cookbook/serializer.py b/cookbook/serializer.py
index 7d6ea556f..5ad8fd0b8 100644
--- a/cookbook/serializer.py
+++ b/cookbook/serializer.py
@@ -164,7 +164,7 @@ class UserPreferenceSerializer(serializers.ModelSerializer):
'user', 'theme', 'nav_color', 'default_unit', 'default_page',
'search_style', 'show_recent', 'plan_share', 'ingredient_decimals',
'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'
)
diff --git a/cookbook/signals.py b/cookbook/signals.py
index b6ddb67bc..1e08bce60 100644
--- a/cookbook/signals.py
+++ b/cookbook/signals.py
@@ -24,7 +24,6 @@ def skip_signal(signal_func):
return _decorator
-# TODO there is probably a way to generalize this
@receiver(post_save, sender=Recipe)
@skip_signal
def update_recipe_search_vector(sender, instance=None, created=False, **kwargs):
diff --git a/cookbook/templates/shopping_list.html b/cookbook/templates/shopping_list.html
index b2b7175cd..be33aafe6 100644
--- a/cookbook/templates/shopping_list.html
+++ b/cookbook/templates/shopping_list.html
@@ -1,21 +1,18 @@
-{% extends "base.html" %}
-{% load django_tables2 %}
-{% load crispy_forms_tags %}
-{% load static %}
-{% load i18n %}
+{% extends "base.html" %} {% comment %} TODO: Deprecate {% endcomment %} {% load django_tables2 %} {% load crispy_forms_tags %} {% load static %} {% load i18n %} {% block title
+%}{% trans "Shopping List" %}{% endblock %} {% block extra_head %} {% include 'include/vue_base.html' %}
-{% block title %}{% trans "Shopping List" %}{% endblock %}
+
+
-{% block extra_head %}
- {% include 'include/vue_base.html' %}
+
+
-
-
+
-
-
+
-
+
+{% endblock %} {% block content %}
@@ -32,943 +29,899 @@
{% trans 'Edit' %}
+
-
-
-
- {% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
-
![]()
- {% else %}
-
- {% endif %}
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
+
+
+ {% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
+
![]()
+ {% else %}
+
+ {% endif %}
+
+
+
+
+
-
-
-
-
-
-
- {% trans 'No recipes selected' %}
-
-
-
-
-
+
+
-
+
+
+
+
+
{% trans 'No recipes selected' %}
+
+
+
+
+
+
+
-
-
+
+
+
| [[c.name]] |
-
-
-
+
+
+
|
[[element.amount.toFixed(2)]] |
[[element.unit.name]] |
[[element.food.name]] |
-
+
|
-
-
+
+
+
-
-
-
-
- {% trans 'Entry Mode' %}
-
+
+
+ {% trans 'Entry Mode' %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% if request.user.userpreference.shopping_auto_sync > 0 %}
-
-
-
- {% trans 'You are offline, shopping list might not syncronize.' %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- {% endif %}
+
+
+
+
+ {% if request.user.userpreference.shopping_auto_sync > 0 %}
+
+
+
{% trans 'You are offline, shopping list might not syncronize.' %}
+
+
+ {% endif %}
-
-
-
+
+
+
+
-
-
-
{% trans 'Export' %}
-
+
+
+
-
-
+{% endblock %} {% block script %}
-
-
-
-
-
+
+
-
+ updateShoppingList: function () {
+ this.loading = true
+ let recipe_promises = []
+
+ for (let i in this.shopping_list.recipes) {
+ if (this.shopping_list.recipes[i].created) {
+ console.log('updating recipe', this.shopping_list.recipes[i])
+ recipe_promises.push(this.$http.post("{% url 'api:shoppinglistrecipe-list' %}", this.shopping_list.recipes[i], {}).then((response) => {
+ let old_id = this.shopping_list.recipes[i].id
+ console.log("list recipe create respose ", response.body)
+ this.$set(this.shopping_list.recipes, i, response.body)
+ for (let e of this.shopping_list.entries.filter(item => item.list_recipe === old_id)) {
+ console.log("found recipe updating ID")
+ e.list_recipe = this.shopping_list.recipes[i].id
+ }
+ }).catch((err) => {
+ console.log(err)
+ this.makeToast(gettext('Error'), gettext("There was an error updating a resource!") + err.bodyText, 'danger')
+ }))
+ }
+ }
+
+ Promise.allSettled(recipe_promises).then(() => {
+ console.log("proceeding to update shopping list", this.shopping_list)
+
+ if (this.shopping_list.id === undefined) {
+ return this.$http.post("{% url 'api:shoppinglist-list' %}", this.shopping_list, {}).then((response) => {
+ console.log(response)
+ this.makeToast(gettext('Updated'), gettext("Object created successfully!"), 'success')
+ this.loading = false
+
+ this.shopping_list = response.body
+ this.shopping_list_id = this.shopping_list.id
+
+ window.history.pushState('shopping_list', '{% trans 'Shopping List' %}', "{% url 'view_shopping' 123456 %}".replace('123456', this.shopping_list_id));
+ }).catch((err) => {
+ console.log(err)
+ this.makeToast(gettext('Error'), gettext("There was an error creating a resource!") + err.bodyText, 'danger')
+ this.loading = false
+ })
+ } else {
+ return this.$http.put("{% url 'api:shoppinglist-detail' 123456 %}".replace('123456', this.shopping_list.id), this.shopping_list, {}).then((response) => {
+ console.log(response)
+ this.shopping_list = response.body
+ this.makeToast(gettext('Updated'), gettext("Changes saved successfully!"), 'success')
+ this.loading = false
+ }).catch((err) => {
+ console.log(err)
+ this.makeToast(gettext('Error'), gettext("There was an error updating a resource!") + err.bodyText, 'danger')
+ this.loading = false
+ })
+ }
+
+
+ })
+ },
+ sortEntries: function (a, b) {
+
+ },
+ dragChanged: function (category, evt) {
+ if (evt.added !== undefined) {
+ if (evt.added.element.id === undefined) {
+ this.makeToast(gettext('Warning'), gettext('This feature is only available after saving the shopping list'), 'warning')
+ } else {
+ this.shopping_list.entries.forEach(entry => {
+ if (entry.id === evt.added.element.id) {
+ if (category.id === -1) {
+ entry.food.supermarket_category = null
+ } else {
+ entry.food.supermarket_category = {
+ name: category.name,
+ id: category.id
+ }
+ }
+ this.$http.put(("{% url 'api:food-detail' 123456 %}").replace('123456', entry.food.id), entry.food).then((response) => {
+
+ }).catch((err) => {
+ this.makeToast(gettext('Error'), gettext('There was an error updating a resource!') + err.bodyText, 'danger')
+ })
+ }
+ })
+ }
+
+
+ }
+ },
+ entryChecked: function (entry) {
+ this.auto_sync_blocked = true
+ let updates = []
+ this.shopping_list.entries.forEach((item) => {
+ if (entry.entries.includes(item.id)) {
+ item.checked = entry.checked
+ updates.push(this.$http.put("{% url 'api:shoppinglistentry-detail' 123456 %}".replace('123456', item.id), item, {}).then((response) => {
+
+ }).catch((err) => {
+ console.log(err)
+ this.makeToast(gettext('Error'), gettext('There was an error updating a resource!') + err.bodyText, 'danger')
+ this.loading = false
+ }))
+ }
+ })
+
+ Promise.allSettled(updates).then(() => {
+ this.auto_sync_blocked = false
+ if (this.unchecked_entries < 1) {
+ this.shopping_list.finished = true
+
+ this.$http.put("{% url 'api:shoppinglist-detail' 123456 %}".replace('123456', this.shopping_list.id), this.shopping_list, {}).then((response) => {
+ this.makeToast(gettext('Finished'), gettext('Shopping list finished!'), 'success')
+ }).catch((err) => {
+ console.log(err)
+ this.makeToast(gettext('Error'), gettext("There was an error updating a resource!") + err.bodyText, 'danger')
+ })
+ } else {
+ if (this.unchecked_entries > 0 && this.shopping_list.finished) {
+ this.shopping_list.finished = false
+
+ this.$http.put("{% url 'api:shoppinglist-detail' 123456 %}".replace('123456', this.shopping_list.id), this.shopping_list, {}).then((response) => {
+ this.makeToast(gettext('Open'), gettext('Shopping list reopened!'), 'success')
+ }).catch((err) => {
+ console.log(err)
+ this.makeToast(gettext('Error'), gettext("There was an error updating a resource!") + err.bodyText, 'danger')
+ })
+ }
+ }
+ })
+ },
+ addEntry: function () {
+ if (this.new_entry.food !== undefined) {
+ this.shopping_list.entries.push({
+ 'list_recipe': null,
+ 'food': this.new_entry.food,
+ 'unit': this.new_entry.unit,
+ 'amount': parseFloat(this.new_entry.amount),
+ 'order': 0,
+ 'checked': false,
+ })
+
+ this.new_entry = {
+ unit: undefined,
+ amount: undefined,
+ food: undefined,
+ }
+
+ this.$refs.new_entry_amount.focus();
+ } else {
+ this.makeToast(gettext('Error'), gettext('Please enter a valid food'), 'danger')
+ }
+ },
+ addSimpleEntry: function () {
+ if (this.simple_entry !== '') {
+
+ this.$http.post('{% url 'api_ingredient_from_string' %}', {text: this.simple_entry}, {emulateJSON: true}).then((response) => {
+
+ console.log(response)
+
+ let unit = null
+ if (response.body.unit !== '') {
+ unit = {'name': response.body.unit}
+ }
+
+ this.shopping_list.entries.push({
+ 'list_recipe': null,
+ 'food': {'name': response.body.food, supermarket_category: null},
+ 'unit': unit,
+ 'amount': response.body.amount,
+ 'order': 0,
+ 'checked': false,
+ })
+
+ this.simple_entry = ''
+ }).catch((err) => {
+ console.log(err)
+ this.makeToast(gettext('Error'), gettext('Something went wrong while trying to add the simple entry.'), 'danger')
+ })
+ }
+ },
+ getRecipes: function () {
+ let url = "{% url 'api:recipe-list' %}?page_size=5&internal=true"
+ if (this.recipe_query !== '') {
+ url += '&query=' + this.recipe_query;
+ } else {
+ this.recipes = []
+ return
+ }
+
+ this.$http.get(url).then((response) => {
+ this.recipes = response.data.results;
+ }).catch((err) => {
+ console.log("getRecipes error: ", err);
+ this.makeToast(gettext('Error'), gettext("There was an error loading a resource!") + err.bodyText, 'danger')
+ })
+ },
+ getRecipeUrl: function (id) {
+ return '{% url 'view_recipe' 123456 %}'.replace('123456', id)
+ },
+ addRecipeToList: function (recipe, servings = 1) {
+ let slr = {
+ "created": true,
+ "id": Math.random() * 1000,
+ "recipe": recipe.id,
+ "recipe_name": recipe.name,
+ "servings": servings,
+ }
+ this.shopping_list.recipes.push(slr)
+
+ this.$http.get('{% url 'api:recipe-detail' 123456 %}'.replace('123456', recipe.id)).then((response) => {
+ for (let s of response.data.steps) {
+ for (let i of s.ingredients) {
+ if (!i.is_header && i.food !== null && i.food.ignore_shopping === false) {
+ this.shopping_list.entries.push({
+ 'list_recipe': slr.id,
+ 'food': i.food,
+ 'unit': i.unit,
+ 'amount': i.amount,
+ 'order': 0
+ })
+ }
+ }
+ }
+ }).catch((err) => {
+ this.makeToast(gettext('Error'), gettext("There was an error loading a resource!") + err.bodyText, 'danger')
+ })
+ },
+ removeRecipeFromList: function (slr) {
+ this.shopping_list.entries = this.shopping_list.entries.filter(item => item.list_recipe !== slr.id)
+ this.shopping_list.recipes = this.shopping_list.recipes.filter(item => item !== slr)
+ },
+ searchKeywords: function (query) {
+ this.keywords_loading = true
+ this.$http.get("{% url 'api:keyword-list' %}" + '?query=' + query + '&limit=10').then((response) => {
+ this.keywords = response.data.results;
+ this.keywords_loading = false
+ }).catch((err) => {
+ console.log(err)
+ this.makeToast(gettext('Error'), gettext("There was an error loading a resource!") + err.bodyText, 'danger')
+ })
+ },
+
+ searchUnits: function (query) {
+ this.units_loading = true
+ this.$http.get("{% url 'api:unit-list' %}" + '?query=' + query + '&limit=10').then((response) => {
+ this.units = response.data.results;
+ this.units_loading = false
+ }).catch((err) => {
+ this.makeToast(gettext('Error'), gettext("There was an error loading a resource!") + err.bodyText, 'danger')
+ })
+ },
+ searchFoods: function (query) {
+ this.foods_loading = true
+ this.$http.get("{% url 'api:food-list' %}" + '?query=' + query + '&limit=10').then((response) => {
+ this.foods = response.data.results
+ this.foods_loading = false
+ }).catch((err) => {
+ this.makeToast(gettext('Error'), gettext("There was an error loading a resource!") + err.bodyText, 'danger')
+ })
+ },
+ addFoodType: function (tag, index) {
+ let new_food = {'name': tag, supermarket_category: null}
+ this.foods.push(new_food)
+ this.new_entry.food = new_food
+ },
+ addUnitType: function (tag, index) {
+ let new_unit = {'name': tag}
+ this.units.push(new_unit)
+ this.new_entry.unit = new_unit
+ },
+ searchUsers: function (query) {
+ this.users_loading = true
+ this.$http.get("{% url 'api:username-list' %}" + '?query=' + query + '&limit=10').then((response) => {
+ this.users = response.data
+ this.users_loading = false
+ }).catch((err) => {
+ this.makeToast(gettext('Error'), gettext("There was an error loading a resource!") + err.bodyText, 'danger')
+ })
+ },
+ searchSupermarket: function (query) { //TODO move to central component
+ this.supermarkets_loading = true
+ this.$http.get("{% url 'api:supermarket-list' %}" + '?query=' + query + '&limit=10').then((response) => {
+ this.supermarkets = response.data
+ this.supermarkets_loading = false
+ }).catch((err) => {
+ this.makeToast(gettext('Error'), gettext("There was an error loading a resource!") + err.bodyText, 'danger')
+ })
+ },
+ },
+ beforeDestroy() {
+ window.removeEventListener('online', this.updateOnlineStatus);
+ window.removeEventListener('offline', this.updateOnlineStatus);
+ }
+ });
+
{% endblock %}
diff --git a/cookbook/views/views.py b/cookbook/views/views.py
index 0eb2a4852..2252acc15 100644
--- a/cookbook/views/views.py
+++ b/cookbook/views/views.py
@@ -387,6 +387,7 @@ def user_settings(request):
up.shopping_auto_sync = shopping_form.cleaned_data['shopping_auto_sync']
up.filter_to_supermarket = shopping_form.cleaned_data['filter_to_supermarket']
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:
up.shopping_auto_sync = settings.SHOPPING_MIN_AUTOSYNC_INTERVAL
up.save()
diff --git a/vue/src/apps/ShoppingListView/ShoppingListView.vue b/vue/src/apps/ShoppingListView/ShoppingListView.vue
index 19a9b58ee..0ad2e824f 100644
--- a/vue/src/apps/ShoppingListView/ShoppingListView.vue
+++ b/vue/src/apps/ShoppingListView/ShoppingListView.vue
@@ -25,24 +25,10 @@
-
+
-
+
@@ -107,7 +93,7 @@
-
+
@@ -183,9 +169,7 @@
-
Drag categories to change the order categories appear in shopping list.
+
{{ $t("CategoryInstruction") }}
+
+
{{ $t("shopping_recent_days") }}
+
+
+
+
+
+
+
+
+ {{ $t("shopping_recent_days_desc") }}
+
+
+
{{ $t("filter_to_supermarket") }}
@@ -459,6 +457,7 @@ import ContextMenuItem from "@/components/ContextMenu/ContextMenuItem"
import ShoppingLineItem from "@/components/ShoppingLineItem"
import GenericMultiselect from "@/components/GenericMultiselect"
import GenericPill from "@/components/GenericPill"
+import LookupInput from "@/components/Modals/LookupInput"
import draggable from "vuedraggable"
import { ApiMixin, getUserPreference } from "@/utils/utils"
@@ -470,7 +469,7 @@ Vue.use(BootstrapVue)
export default {
name: "ShoppingListView",
mixins: [ApiMixin],
- components: { ContextMenu, ContextMenuItem, ShoppingLineItem, GenericMultiselect, GenericPill, draggable },
+ components: { ContextMenu, ContextMenuItem, ShoppingLineItem, GenericMultiselect, GenericPill, draggable, LookupInput },
data() {
return {
@@ -492,6 +491,7 @@ export default {
mealplan_autoinclude_related: false,
mealplan_autoexclude_onhand: true,
filter_to_supermarket: false,
+ shopping_recent_days: 7,
},
new_supermarket: { entrymode: false, value: undefined, editmode: undefined },
new_category: { entrymode: false, value: undefined },
@@ -577,6 +577,16 @@ export default {
defaultDelay() {
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() {
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: {
// this.genericAPI inherited from ApiMixin
+ test(e) {
+ this.new_item.unit = e
+ console.log(e, this.new_item, this.formUnit)
+ },
addItem() {
let api = new ApiApiFactory()
api.createShoppingListEntry(this.new_item)
diff --git a/vue/src/apps/SupermarketView/SupermarketView.vue b/vue/src/apps/SupermarketView/SupermarketView.vue
index 2559a1429..821504c9f 100644
--- a/vue/src/apps/SupermarketView/SupermarketView.vue
+++ b/vue/src/apps/SupermarketView/SupermarketView.vue
@@ -1,201 +1,178 @@
+
+
+
+
+
{{ $t("Supermarket") }}
-
+
-
+
+ {{ $t("Edit") }}
+
+ {{ $t("New") }}
+
+
-
-
{{ $t('Supermarket') }}
+
-
-
+
+
+
+ {{ $t("Categories") }}
+
+
-
- {{ $t('Edit') }}
-
- {{ $t('New') }}
-
-
+
+
+
+
+
+
+
+
{{ $t("Selected") }} {{ $t("Categories") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
{{ $t('Categories') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{{ $t('Selected') }} {{ $t('Categories') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/vue/src/components/Modals/GenericModalForm.vue b/vue/src/components/Modals/GenericModalForm.vue
index 1e2d22379..4855f89bc 100644
--- a/vue/src/components/Modals/GenericModalForm.vue
+++ b/vue/src/components/Modals/GenericModalForm.vue
@@ -8,7 +8,6 @@
{{ f.label }}
-
diff --git a/vue/src/locales/en.json b/vue/src/locales/en.json
index 36385620b..d2546dff5 100644
--- a/vue/src/locales/en.json
+++ b/vue/src/locales/en.json
@@ -238,7 +238,6 @@
"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.",
"filter_to_supermarket": "Filter to Supermarket",
- "filter_to_supermarket_desc": "Filter shopping list to only include supermarket categories.",
"Week_Numbers": "Week numbers",
"Show_Week_Numbers": "Show week numbers ?",
"Export_As_ICal": "Export current period to iCal format",
@@ -260,6 +259,10 @@
"nothing": "Nothing to do",
"err_merge_self": "Cannot merge item with itself",
"show_sql": "Show SQL",
+ "filter_to_supermarket_desc": "By default, filter shopping list to only include categories for selected supermarket.",
"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"
}
diff --git a/vue/src/utils/models.js b/vue/src/utils/models.js
index 312e6cc91..ab94efdc6 100644
--- a/vue/src/utils/models.js
+++ b/vue/src/utils/models.js
@@ -216,6 +216,24 @@ export class Models {
},
create: {
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,
+ },
+ },
},
}