From a217db58225cd34f174ac7b7ea30af01083a2106 Mon Sep 17 00:00:00 2001 From: smilerz Date: Sun, 31 Oct 2021 13:33:15 -0500 Subject: [PATCH] add new unit/food from shopping list --- cookbook/forms.py | 5 +- cookbook/helper/permission_helper.py | 3 - cookbook/helper/recipe_search.py | 11 - cookbook/helper/shopping_helper.py | 8 +- .../migrations/0143_build_full_text_index.py | 10 +- .../0159_add_shoppinglistentry_fields.py | 5 + cookbook/models.py | 3 +- cookbook/serializer.py | 2 +- cookbook/signals.py | 1 - cookbook/templates/shopping_list.html | 1715 ++++++++--------- cookbook/views/views.py | 1 + .../ShoppingListView/ShoppingListView.vue | 56 +- .../apps/SupermarketView/SupermarketView.vue | 321 ++- .../components/Modals/GenericModalForm.vue | 1 - vue/src/locales/en.json | 7 +- vue/src/utils/models.js | 18 + 16 files changed, 1061 insertions(+), 1106 deletions(-) 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' %} +
+ +

{% trans 'Shopping List' %}

+
+ + + + + +
+ {% trans 'Edit' %}
+
-