Squashed commit of shoppinglist_v2

This commit is contained in:
smilerz
2021-12-30 15:33:34 -06:00
parent 67e4c88be7
commit 957c659a62
49 changed files with 701 additions and 1472 deletions

View File

@@ -259,7 +259,7 @@ admin.site.register(ViewLog, ViewLogAdmin)
class InviteLinkAdmin(admin.ModelAdmin):
list_display = (
'group', 'valid_until',
'group', 'valid_until', 'space',
'created_by', 'created_at', 'used_by'
)

View File

@@ -14,24 +14,24 @@ class CookbookConfig(AppConfig):
def ready(self):
import cookbook.signals # noqa
if not settings.DISABLE_TREE_FIX_STARTUP:
# when starting up run fix_tree to:
# a) make sure that nodes are sorted when switching between sort modes
# b) fix problems, if any, with tree consistency
with scopes_disabled():
try:
from cookbook.models import Food, Keyword
Keyword.fix_tree(fix_paths=True)
Food.fix_tree(fix_paths=True)
except OperationalError:
if DEBUG:
traceback.print_exc()
pass # if model does not exist there is no need to fix it
except ProgrammingError:
if DEBUG:
traceback.print_exc()
pass # if migration has not been run database cannot be fixed yet
except Exception:
if DEBUG:
traceback.print_exc()
pass # dont break startup just because fix could not run, need to investigate cases when this happens
# if not settings.DISABLE_TREE_FIX_STARTUP:
# # when starting up run fix_tree to:
# # a) make sure that nodes are sorted when switching between sort modes
# # b) fix problems, if any, with tree consistency
# with scopes_disabled():
# try:
# from cookbook.models import Food, Keyword
# Keyword.fix_tree(fix_paths=True)
# Food.fix_tree(fix_paths=True)
# except OperationalError:
# if DEBUG:
# traceback.print_exc()
# pass # if model does not exist there is no need to fix it
# except ProgrammingError:
# if DEBUG:
# traceback.print_exc()
# pass # if migration has not been run database cannot be fixed yet
# except Exception:
# if DEBUG:
# traceback.print_exc()
# pass # dont break startup just because fix could not run, need to investigate cases when this happens

View File

@@ -7,7 +7,7 @@ from django_scopes import scopes_disabled
from django_scopes.forms import SafeModelChoiceField, SafeModelMultipleChoiceField
from hcaptcha.fields import hCaptchaField
from .models import (Comment, InviteLink, Keyword, MealPlan, MealType, Recipe, RecipeBook,
from .models import (Comment, Food, InviteLink, Keyword, MealPlan, MealType, Recipe, RecipeBook,
RecipeBookEntry, SearchPreference, Space, Storage, Sync, User, UserPreference)
@@ -155,13 +155,14 @@ class ImportExportBase(forms.Form):
OPENEATS = 'OPENEATS'
PLANTOEAT = 'PLANTOEAT'
COOKBOOKAPP = 'COOKBOOKAPP'
COPYMETHAT = 'COPYMETHAT'
type = forms.ChoiceField(choices=(
(DEFAULT, _('Default')), (PAPRIKA, 'Paprika'), (NEXTCLOUD, 'Nextcloud Cookbook'),
(MEALIE, 'Mealie'), (CHOWDOWN, 'Chowdown'), (SAFRON, 'Safron'), (CHEFTAP, 'ChefTap'),
(PEPPERPLATE, 'Pepperplate'), (RECETTETEK, 'RecetteTek'), (RECIPESAGE, 'Recipe Sage'), (DOMESTICA, 'Domestica'),
(MEALMASTER, 'MealMaster'), (REZKONV, 'RezKonv'), (OPENEATS, 'Openeats'), (RECIPEKEEPER, 'Recipe Keeper'),
(PLANTOEAT, 'Plantoeat'), (COOKBOOKAPP, 'CookBookApp'),
(PLANTOEAT, 'Plantoeat'), (COOKBOOKAPP, 'CookBookApp'), (COPYMETHAT, 'CopyMeThat'),
))
@@ -524,7 +525,7 @@ class SpacePreferenceForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) # populates the post
self.fields['food_inherit'].queryset = Food.inherit_fields
self.fields['food_inherit'].queryset = Food.inheritable_fields
class Meta:
model = Space

View File

@@ -1,13 +1,14 @@
import json
import re
from json import JSONDecodeError
from urllib.parse import unquote
from bs4 import BeautifulSoup
from bs4.element import Tag
from recipe_scrapers._utils import get_host_name, normalize_string
from cookbook.helper import recipe_url_import as helper
from cookbook.helper.scrapers.scrapers import text_scraper
from json import JSONDecodeError
from recipe_scrapers._utils import get_host_name, normalize_string
from urllib.parse import unquote
def get_recipe_from_source(text, url, request):
@@ -58,7 +59,7 @@ def get_recipe_from_source(text, url, request):
return kid_list
recipe_json = {
'name': '',
'name': '',
'url': '',
'description': '',
'image': '',
@@ -188,6 +189,6 @@ def remove_graph(el):
for x in el['@graph']:
if '@type' in x and x['@type'] == 'Recipe':
el = x
except TypeError:
except (TypeError, JSONDecodeError):
pass
return el

View File

@@ -82,7 +82,7 @@ def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None
ingredients = Ingredient.objects.filter(step__recipe=r, space=space)
if exclude_onhand := created_by.userpreference.mealplan_autoexclude_onhand:
ingredients = ingredients.exclude(food__on_hand=True)
ingredients = ingredients.exclude(food__food_onhand=True)
if related := created_by.userpreference.mealplan_autoinclude_related:
# TODO: add levels of related recipes (related recipes of related recipes) to use when auto-adding mealplans
@@ -93,7 +93,7 @@ def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None
# TODO once/if Steps can have a serving size this needs to be refactored
if exclude_onhand:
# if steps are used more than once in a recipe or subrecipe - I don' think this results in the desired behavior
related_step_ing += Ingredient.objects.filter(step__recipe=x, food__on_hand=False, space=space).values_list('id', flat=True)
related_step_ing += Ingredient.objects.filter(step__recipe=x, food__food_onhand=False, space=space).values_list('id', flat=True)
else:
related_step_ing += Ingredient.objects.filter(step__recipe=x, space=space).values_list('id', flat=True)
@@ -101,10 +101,10 @@ def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None
if ingredients.filter(food__recipe=x).exists():
for ing in ingredients.filter(food__recipe=x):
if exclude_onhand:
x_ing = Ingredient.objects.filter(step__recipe=x, food__on_hand=False, space=space)
x_ing = Ingredient.objects.filter(step__recipe=x, food__food_onhand=False, space=space)
else:
x_ing = Ingredient.objects.filter(step__recipe=x, space=space)
for i in [x for x in x_ing if not x.food.ignore_shopping]:
for i in [x for x in x_ing]:
ShoppingListEntry.objects.create(
list_recipe=list_recipe,
food=i.food,
@@ -139,7 +139,7 @@ def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None
sle.save()
# add any missing Entrys
for i in [x for x in add_ingredients if x.food and not x.food.ignore_shopping]:
for i in [x for x in add_ingredients if x.food]:
ShoppingListEntry.objects.create(
list_recipe=list_recipe,

View File

@@ -0,0 +1,84 @@
import re
from io import BytesIO
from zipfile import ZipFile
from bs4 import BeautifulSoup
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.recipe_html_import import get_recipe_from_source
from cookbook.helper.recipe_url_import import iso_duration_to_minutes, parse_servings
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient, Keyword
from recipes.settings import DEBUG
class CopyMeThat(Integration):
def import_file_name_filter(self, zip_info_object):
if DEBUG:
print("testing", zip_info_object.filename, zip_info_object.filename == 'recipes.html')
return zip_info_object.filename == 'recipes.html'
def get_recipe_from_file(self, file):
# 'file' comes is as a beautifulsoup object
recipe = Recipe.objects.create(name=file.find("div", {"id": "name"}).text.strip(), created_by=self.request.user, internal=True, space=self.request.space, )
for category in file.find_all("span", {"class": "recipeCategory"}):
keyword, created = Keyword.objects.get_or_create(name=category.text, space=self.request.space)
recipe.keywords.add(keyword)
try:
recipe.servings = parse_servings(file.find("a", {"id": "recipeYield"}).text.strip())
recipe.working_time = iso_duration_to_minutes(file.find("span", {"meta": "prepTime"}).text.strip())
recipe.waiting_time = iso_duration_to_minutes(file.find("span", {"meta": "cookTime"}).text.strip())
recipe.save()
except AttributeError:
pass
step = Step.objects.create(instruction='', space=self.request.space, )
ingredient_parser = IngredientParser(self.request, True)
for ingredient in file.find_all("li", {"class": "recipeIngredient"}):
if ingredient.text == "":
continue
amount, unit, ingredient, note = ingredient_parser.parse(ingredient.text.strip())
f = ingredient_parser.get_food(ingredient)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))
for s in file.find_all("li", {"class": "instruction"}):
if s.text == "":
continue
step.instruction += s.text.strip() + ' \n\n'
for s in file.find_all("li", {"class": "recipeNote"}):
if s.text == "":
continue
step.instruction += s.text.strip() + ' \n\n'
try:
if file.find("a", {"id": "original_link"}).text != '':
step.instruction += "\n\nImported from: " + file.find("a", {"id": "original_link"}).text
step.save()
except AttributeError:
pass
recipe.steps.add(step)
# import the Primary recipe image that is stored in the Zip
try:
for f in self.files:
if '.zip' in f['name']:
import_zip = ZipFile(f['file'])
self.import_recipe_image(recipe, BytesIO(import_zip.read(file.find("img", class_="recipeImage").get("src"))), filetype='.jpeg')
except Exception as e:
print(recipe.name, ': failed to import image ', str(e))
recipe.save()
return recipe
def split_recipe_file(self, file):
soup = BeautifulSoup(file, "html.parser")
return soup.find_all("div", {"class": "recipe"})

View File

@@ -5,6 +5,7 @@ import uuid
from io import BytesIO, StringIO
from zipfile import BadZipFile, ZipFile
from bs4 import Tag
from django.core.exceptions import ObjectDoesNotExist
from django.core.files import File
from django.db import IntegrityError
@@ -16,7 +17,7 @@ from django_scopes import scope
from cookbook.forms import ImportExportBase
from cookbook.helper.image_processing import get_filetype, handle_image
from cookbook.models import Keyword, Recipe
from recipes.settings import DATABASES, DEBUG
from recipes.settings import DEBUG
class Integration:
@@ -153,9 +154,17 @@ class Integration:
file_list.append(z)
il.total_recipes += len(file_list)
import cookbook
if isinstance(self, cookbook.integration.copymethat.CopyMeThat):
file_list = self.split_recipe_file(BytesIO(import_zip.read('recipes.html')))
il.total_recipes += len(file_list)
for z in file_list:
try:
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
if isinstance(z, Tag):
recipe = self.get_recipe_from_file(z)
else:
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
recipe.keywords.add(self.keyword)
il.msg += f'{recipe.pk} - {recipe.name} \n'
self.handle_duplicates(recipe, import_duplicates)

View File

@@ -28,11 +28,6 @@ class Migration(migrations.Migration):
]
operations = [
migrations.AddField(
model_name='food',
name='on_hand',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='shoppinglistentry',
name='completed_at',
@@ -105,11 +100,6 @@ class Migration(migrations.Migration):
],
bases=(models.Model, PermissionModelMixin),
),
migrations.AddField(
model_name='food',
name='inherit',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='userpreference',
name='mealplan_autoinclude_related',
@@ -117,7 +107,7 @@ class Migration(migrations.Migration):
),
migrations.AddField(
model_name='food',
name='ignore_inherit',
name='inherit_fields',
field=models.ManyToManyField(blank=True, to='cookbook.FoodInheritField'),
),
migrations.AddField(
@@ -145,5 +135,10 @@ class Migration(migrations.Migration):
name='shopping_recent_days',
field=models.PositiveIntegerField(default=7),
),
migrations.RenameField(
model_name='food',
old_name='ignore_shopping',
new_name='food_onhand',
),
migrations.RunPython(copy_values_to_sle),
]

View File

@@ -21,7 +21,7 @@ def delete_orphaned_sle(apps, schema_editor):
def create_inheritfields(apps, schema_editor):
FoodInheritField.objects.create(name='Supermarket Category', field='supermarket_category')
FoodInheritField.objects.create(name='Ignore Shopping', field='ignore_shopping')
FoodInheritField.objects.create(name='On Hand', field='food_onhand')
FoodInheritField.objects.create(name='Diet', field='diet')
FoodInheritField.objects.create(name='Substitute', field='substitute')
FoodInheritField.objects.create(name='Substitute Children', field='substitute_children')

View File

@@ -482,7 +482,7 @@ class Unit(ExportModelOperationsMixin('unit'), models.Model, PermissionModelMixi
class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
# exclude fields not implemented yet
inherit_fields = FoodInheritField.objects.exclude(field__in=['diet', 'substitute', 'substitute_children', 'substitute_siblings'])
inheritable_fields = FoodInheritField.objects.exclude(field__in=['diet', 'substitute', 'substitute_children', 'substitute_siblings'])
# WARNING: Food inheritance relies on post_save signals, avoid using UPDATE to update Food objects unless you intend to bypass those signals
if SORT_TREE_BY_NAME:
@@ -490,11 +490,9 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
name = models.CharField(max_length=128, validators=[MinLengthValidator(1)])
recipe = models.ForeignKey('Recipe', null=True, blank=True, on_delete=models.SET_NULL)
supermarket_category = models.ForeignKey(SupermarketCategory, null=True, blank=True, on_delete=models.SET_NULL)
ignore_shopping = models.BooleanField(default=False) # inherited field
food_onhand = models.BooleanField(default=False) # inherited field
description = models.TextField(default='', blank=True)
on_hand = models.BooleanField(default=False)
inherit = models.BooleanField(default=False)
ignore_inherit = models.ManyToManyField(FoodInheritField, blank=True) # inherited field: is this name better as inherit instead of ignore inherit? which is more intuitive?
inherit_fields = models.ManyToManyField(FoodInheritField, blank=True) # inherited field: is this name better as inherit instead of ignore inherit? which is more intuitive?
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space', _manager_class=TreeManager)
@@ -512,34 +510,30 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
def reset_inheritance(space=None):
# resets inheritted fields to the space defaults and updates all inheritted fields to root object values
inherit = space.food_inherit.all()
ignore_inherit = Food.inherit_fields.difference(inherit)
# remove all inherited fields from food
Through = Food.objects.filter(space=space).first().inherit_fields.through
Through.objects.all().delete()
# food is going to inherit attributes
if space.food_inherit.all().count() > 0:
# using update to avoid creating a N*depth! save signals
Food.objects.filter(space=space).update(inherit=True)
# ManyToMany cannot be updated through an UPDATE operation
Through = Food.objects.first().ignore_inherit.through
Through.objects.all().delete()
for i in ignore_inherit:
for i in inherit:
Through.objects.bulk_create([
Through(food_id=x, foodinheritfield_id=i.id)
for x in Food.objects.filter(space=space).values_list('id', flat=True)
])
inherit = inherit.values_list('field', flat=True)
if 'ignore_shopping' in inherit:
if 'food_onhand' in inherit:
# get food at root that have children that need updated
Food.include_descendants(queryset=Food.objects.filter(depth=1, numchild__gt=0, space=space, ignore_shopping=True)).update(ignore_shopping=True)
Food.include_descendants(queryset=Food.objects.filter(depth=1, numchild__gt=0, space=space, ignore_shopping=False)).update(ignore_shopping=False)
Food.include_descendants(queryset=Food.objects.filter(depth=1, numchild__gt=0, space=space, food_onhand=True)).update(food_onhand=True)
Food.include_descendants(queryset=Food.objects.filter(depth=1, numchild__gt=0, space=space, food_onhand=False)).update(food_onhand=False)
if 'supermarket_category' in inherit:
# when supermarket_category is null or blank assuming it is not set and not intended to be blank for all descedants
# find top node that has category set
category_roots = Food.exclude_descendants(queryset=Food.objects.filter(supermarket_category__isnull=False, numchild__gt=0, space=space))
for root in category_roots:
root.get_descendants().update(supermarket_category=root.supermarket_category)
else: # food is not going to inherit any attributes
Food.objects.filter(space=space).update(inherit=False)
class Meta:
constraints = [

View File

@@ -157,15 +157,9 @@ class FoodInheritFieldSerializer(WritableNestedModelSerializer):
class UserPreferenceSerializer(serializers.ModelSerializer):
# food_inherit_default = FoodInheritFieldSerializer(source='space.food_inherit', read_only=True)
food_ignore_default = serializers.SerializerMethodField('get_ignore_default')
food_inherit_default = FoodInheritFieldSerializer(source='space.food_inherit', many=True, allow_null=True, required=False, read_only=True)
plan_share = UserNameSerializer(many=True, allow_null=True, required=False, read_only=True)
# TODO decide: default inherit field values for foods are being handled via VUE client through user preference
# should inherit field instead be set during the django model create?
def get_ignore_default(self, obj):
return FoodInheritFieldSerializer(Food.inherit_fields.difference(obj.space.food_inherit.all()), many=True).data
def create(self, validated_data):
if not validated_data.get('user', None):
raise ValidationError(_('A user is required'))
@@ -181,7 +175,7 @@ class UserPreferenceSerializer(serializers.ModelSerializer):
model = UserPreference
fields = (
'user', 'theme', 'nav_color', 'default_unit', 'default_page', 'use_kj', 'search_style', 'show_recent', 'plan_share',
'ingredient_decimals', 'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'food_ignore_default', 'default_delay',
'ingredient_decimals', 'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'food_inherit_default', 'default_delay',
'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share', 'shopping_recent_days', 'csv_delim', 'csv_prefix', 'filter_to_supermarket'
)
@@ -305,7 +299,7 @@ class KeywordSerializer(UniqueFieldsMixin, ExtendedRecipeMixin):
model = Keyword
fields = (
'id', 'name', 'icon', 'label', 'description', 'image', 'parent', 'numchild', 'numrecipe', 'created_at',
'updated_at')
'updated_at', 'full_name')
read_only_fields = ('id', 'label', 'numchild', 'parent', 'image')
@@ -376,7 +370,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
supermarket_category = SupermarketCategorySerializer(allow_null=True, required=False)
recipe = RecipeSimpleSerializer(allow_null=True, required=False)
shopping = serializers.SerializerMethodField('get_shopping_status')
ignore_inherit = FoodInheritFieldSerializer(many=True, allow_null=True, required=False)
inherit_fields = FoodInheritFieldSerializer(many=True, allow_null=True, required=False)
recipe_filter = 'steps__ingredients__food'
@@ -402,8 +396,8 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
class Meta:
model = Food
fields = (
'id', 'name', 'description', 'shopping', 'recipe', 'ignore_shopping', 'supermarket_category',
'image', 'parent', 'numchild', 'numrecipe', 'on_hand', 'inherit', 'ignore_inherit',
'id', 'name', 'description', 'shopping', 'recipe', 'food_onhand', 'supermarket_category',
'image', 'parent', 'numchild', 'numrecipe', 'inherit_fields', 'full_name'
)
read_only_fields = ('id', 'numchild', 'parent', 'image', 'numrecipe')
@@ -867,7 +861,7 @@ class FoodExportSerializer(FoodSerializer):
class Meta:
model = Food
fields = ('name', 'ignore_shopping', 'supermarket_category', 'on_hand')
fields = ('name', 'food_onhand', 'supermarket_category',)
class IngredientExportSerializer(WritableNestedModelSerializer):

View File

@@ -66,17 +66,17 @@ def update_food_inheritance(sender, instance=None, created=False, **kwargs):
if not instance:
return
inherit = Food.inherit_fields.difference(instance.ignore_inherit.all())
inherit = instance.inherit_fields.all()
# nothing to apply from parent and nothing to apply to children
if (not instance.inherit or not instance.parent or inherit.count() == 0) and instance.numchild == 0:
if (not instance.parent or inherit.count() == 0) and instance.numchild == 0:
return
inherit = inherit.values_list('field', flat=True)
# apply changes from parent to instance for each inheritted field
if instance.inherit and instance.parent and inherit.count() > 0:
if instance.parent and inherit.count() > 0:
parent = instance.get_parent()
if 'ignore_shopping' in inherit:
instance.ignore_shopping = parent.ignore_shopping
if 'food_onhand' in inherit:
instance.food_onhand = parent.food_onhand
# if supermarket_category is not set, do not cascade - if this becomes non-intuitive can change
if 'supermarket_category' in inherit and parent.supermarket_category:
instance.supermarket_category = parent.supermarket_category
@@ -89,13 +89,13 @@ def update_food_inheritance(sender, instance=None, created=False, **kwargs):
# TODO figure out how to generalize this
# apply changes to direct children - depend on save signals for those objects to cascade inheritance down
_save = []
for child in instance.get_children().filter(inherit=True).exclude(ignore_inherit__field='ignore_shopping'):
child.ignore_shopping = instance.ignore_shopping
for child in instance.get_children().filter(inherit_fields__field='food_onhand'):
child.food_onhand = instance.food_onhand
_save.append(child)
# don't cascade empty supermarket category
if instance.supermarket_category:
# apply changes to direct children - depend on save signals for those objects to cascade inheritance down
for child in instance.get_children().filter(inherit=True).exclude(ignore_inherit__field='supermarket_category'):
for child in instance.get_children().filter(inherit_fields__field='supermarket_category'):
child.supermarket_category = instance.supermarket_category
_save.append(child)
for child in set(_save):

File diff suppressed because one or more lines are too long

View File

@@ -67,7 +67,7 @@
</button>
{% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
<a class="navbar-brand p-0 me-2 justify-content-center" href="/" aria-label="Tandoor">
<a class="navbar-brand p-0 me-2 justify-content-center" href="{% base_path request 'base' %}" aria-label="Tandoor">
<img class="brand-icon" src="{% static 'assets/brand_logo.png' %}" alt="" style="height: 5vh;">
</a>
{% endif %}

View File

@@ -834,7 +834,7 @@
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) {
if (!i.is_header && i.food !== null && i.food.food_onhand === false) {
this.shopping_list.entries.push({
'list_recipe': slr.id,
'food': i.food,

View File

@@ -76,6 +76,7 @@
<option value="CHEFTAP">Cheftap</option>
<option value="CHOWDOWN">Chowdown</option>
<option value="COOKBOOKAPP">CookBookApp</option>
<option value="COPYMETHAT">CopyMeThat</option>
<option value="DOMESTICA">Domestica</option>
<option value="MEALIE">Mealie</option>
<option value="MEALMASTER">Mealmaster</option>

View File

@@ -57,7 +57,19 @@ def obj_tree_1(request, space_1):
except AttributeError:
params = {}
objs = []
inherit = params.pop('inherit', False)
objs.extend(FoodFactory.create_batch(3, space=space_1, **params))
# set all foods to inherit everything
if inherit:
inherit = Food.inheritable_fields
Through = Food.objects.filter(space=space_1).first().inherit_fields.through
for i in inherit:
Through.objects.bulk_create([
Through(food_id=x, foodinheritfield_id=i.id)
for x in Food.objects.filter(space=space_1).values_list('id', flat=True)
])
objs[0].move(objs[1], node_location)
objs[1].move(objs[2], node_location)
return Food.objects.get(id=objs[1].id) # whenever you move/merge a tree it's safest to re-get the object
@@ -471,8 +483,8 @@ def test_tree_filter(obj_tree_1, obj_2, obj_3, u1_s1):
@pytest.mark.parametrize("obj_tree_1, field, inherit, new_val", [
({'has_category': True, 'inherit': True}, 'supermarket_category', True, 'cat_1'),
({'has_category': True, 'inherit': False}, 'supermarket_category', False, 'cat_1'),
({'ignore_shopping': True, 'inherit': True}, 'ignore_shopping', True, 'false'),
({'ignore_shopping': True, 'inherit': False}, 'ignore_shopping', False, 'false'),
({'food_onhand': True, 'inherit': True}, 'food_onhand', True, 'false'),
({'food_onhand': True, 'inherit': False}, 'food_onhand', False, 'false'),
], indirect=['obj_tree_1']) # indirect=True populates magic variable request.param of obj_tree_1 with the parameter
def test_inherit(request, obj_tree_1, field, inherit, new_val, u1_s1):
with scope(space=obj_tree_1.space):
@@ -496,47 +508,17 @@ def test_inherit(request, obj_tree_1, field, inherit, new_val, u1_s1):
assert (getattr(child, field) == new_val) == inherit
@pytest.mark.parametrize("obj_tree_1, field, inherit, new_val", [
({'has_category': True, 'inherit': True, }, 'supermarket_category', True, 'cat_1'),
({'ignore_shopping': True, 'inherit': True, }, 'ignore_shopping', True, 'false'),
], indirect=['obj_tree_1'])
# This is more about the model than the API - should this be moved to a different test?
def test_ignoreinherit_field(request, obj_tree_1, field, inherit, new_val, u1_s1):
with scope(space=obj_tree_1.space):
parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
obj_tree_1.ignore_inherit.add(FoodInheritField.objects.get(field=field))
new_val = request.getfixturevalue(new_val)
# change parent to a new value
setattr(parent, field, new_val)
with scope(space=parent.space):
parent.save() # trigger post-save signal
# get the objects again because values are cached
obj_tree_1 = Food.objects.get(id=obj_tree_1.id)
# inheritance is blocked - should not get new value
assert getattr(obj_tree_1, field) != new_val
setattr(obj_tree_1, field, new_val)
with scope(space=parent.space):
obj_tree_1.save() # trigger post-save signal
# get the objects again because values are cached
child = Food.objects.get(id=child.id)
# inherit with child should still work
assert getattr(child, field) == new_val
@pytest.mark.parametrize("obj_tree_1", [
({'has_category': True, 'inherit': False, 'ignore_shopping': True}),
({'has_category': True, 'inherit': False, 'food_onhand': True}),
], indirect=['obj_tree_1'])
def test_reset_inherit(obj_tree_1, space_1):
with scope(space=space_1):
space_1.food_inherit.add(*Food.inherit_fields.values_list('id', flat=True)) # set default inherit fields
space_1.food_inherit.add(*Food.inheritable_fields.values_list('id', flat=True)) # set default inherit fields
parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
obj_tree_1.ignore_shopping = False
assert parent.ignore_shopping == child.ignore_shopping
assert parent.ignore_shopping != obj_tree_1.ignore_shopping
obj_tree_1.food_onhand = False
assert parent.food_onhand == child.food_onhand
assert parent.food_onhand != obj_tree_1.food_onhand
assert parent.supermarket_category != child.supermarket_category
assert parent.supermarket_category != obj_tree_1.supermarket_category
@@ -545,5 +527,5 @@ def test_reset_inherit(obj_tree_1, space_1):
obj_tree_1 = Food.objects.get(id=obj_tree_1.id)
parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
assert parent.ignore_shopping == obj_tree_1.ignore_shopping == child.ignore_shopping
assert parent.food_onhand == obj_tree_1.food_onhand == child.food_onhand
assert parent.supermarket_category == obj_tree_1.supermarket_category == child.supermarket_category

View File

@@ -3,6 +3,8 @@ from datetime import timedelta
import factory
import pytest
# work around for bug described here https://stackoverflow.com/a/70312265/15762829
from django.conf import settings
from django.contrib import auth
from django.forms import model_to_dict
from django.urls import reverse
@@ -14,6 +16,11 @@ from cookbook.models import Food, Ingredient, ShoppingListEntry, Step
from cookbook.tests.factories import (IngredientFactory, MealPlanFactory, RecipeFactory,
StepFactory, UserFactory)
if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2',
'django.db.backends.postgresql']:
from django.db.backends.postgresql.features import DatabaseFeatures
DatabaseFeatures.can_defer_constraint_checks = False
SHOPPING_LIST_URL = 'api:shoppinglistentry-list'
SHOPPING_RECIPE_URL = 'api:recipe-shopping'
@@ -43,7 +50,7 @@ def recipe(request, space_1, u1_s1):
# steps__food_recipe_count = params.get('steps__food_recipe_count', {})
params['created_by'] = params.get('created_by', auth.get_user(u1_s1))
params['space'] = space_1
return RecipeFactory.create(**params)
return RecipeFactory(**params)
# return RecipeFactory.create(
# steps__recipe_count=steps__recipe_count,
@@ -178,27 +185,24 @@ def test_shopping_recipe_edit(request, recipe, sle_count, use_mealplan, u1_s1, u
@pytest.mark.parametrize("user2, sle_count", [
({'mealplan_autoadd_shopping': False}, (0, 17)),
({'mealplan_autoinclude_related': False}, (8, 8)),
({'mealplan_autoexclude_onhand': False}, (19, 19)),
({'mealplan_autoexclude_onhand': False, 'mealplan_autoinclude_related': False}, (9, 9)),
({'mealplan_autoadd_shopping': False}, (0, 18)),
({'mealplan_autoinclude_related': False}, (9, 9)),
({'mealplan_autoexclude_onhand': False}, (20, 20)),
({'mealplan_autoexclude_onhand': False, 'mealplan_autoinclude_related': False}, (10, 10)),
], indirect=['user2'])
@pytest.mark.parametrize("use_mealplan", [(False), (True), ])
@pytest.mark.parametrize("recipe", [({'steps__recipe_count': 1})], indirect=['recipe'])
def test_shopping_recipe_userpreference(recipe, sle_count, use_mealplan, user2):
with scopes_disabled():
user = auth.get_user(user2)
# setup recipe with 10 ingredients, 1 step recipe with 10 ingredients, 2 food onhand(from recipe and step_recipe), 1 food ignore shopping
# setup recipe with 10 ingredients, 1 step recipe with 10 ingredients, 2 food onhand(from recipe and step_recipe)
ingredients = Ingredient.objects.filter(step__recipe=recipe)
food = Food.objects.get(id=ingredients[2].food.id)
food.on_hand = True
food.food_onhand = True
food.save()
food = recipe.steps.filter(type=Step.RECIPE).first().step_recipe.steps.first().ingredients.first().food
food = Food.objects.get(id=food.id)
food.on_hand = True
food.save()
food = Food.objects.get(id=ingredients[4].food.id)
food.ignore_shopping = True
food.food_onhand = True
food.save()
if use_mealplan:

View File

@@ -112,30 +112,29 @@ def test_preference_delete(u1_s1, u2_s1):
def test_default_inherit_fields(u1_s1, u1_s2, space_1, space_2):
food_inherit_fields = Food.inherit_fields.all()
r = u1_s1.get(
reverse(DETAIL_URL, args={auth.get_user(u1_s1).id}),
)
food_inherit_fields = Food.inheritable_fields
assert len([x.field for x in food_inherit_fields]) > 0
# by default space food will not inherit any fields, so all of them will be ignored
assert space_1.food_inherit.all().count() == 0
assert len([x.field for x in food_inherit_fields]) == len([x['field'] for x in json.loads(r.content)['food_ignore_default']]) > 0
# inherit all possible fields
with scope(space=space_1):
space_1.food_inherit.add(*Food.inherit_fields.values_list('id', flat=True))
r = u1_s1.get(
reverse(DETAIL_URL, args={auth.get_user(u1_s1).id}),
)
assert len([x['field'] for x in json.loads(r.content)['food_inherit_default']]) == 0
assert space_1.food_inherit.all().count() == Food.inherit_fields.all().count() > 0
# now by default, food is not ignoring inheritance on any field
assert len([x['field'] for x in json.loads(r.content)['food_ignore_default']]) == 0
# inherit all possible fields
with scope(space=space_1):
space_1.food_inherit.add(*Food.inheritable_fields.values_list('id', flat=True))
# other spaces and users in those spaced not effected
assert space_1.food_inherit.all().count() == Food.inheritable_fields.count() > 0
# now by default, food is inheriting all of the possible fields
r = u1_s1.get(
reverse(DETAIL_URL, args={auth.get_user(u1_s1).id}),
)
assert len([x['field'] for x in json.loads(r.content)['food_inherit_default']]) == space_1.food_inherit.all().count()
# other spaces and users in those spaces not effected
r = u1_s2.get(
reverse(DETAIL_URL, args={auth.get_user(u1_s2).id}),
)
assert space_2.food_inherit.all().count() == 0
assert len([x.field for x in food_inherit_fields]) == len([x['field'] for x in json.loads(r.content)['food_ignore_default']]) > 0
assert space_2.food_inherit.all().count() == 0 == len([x['field'] for x in json.loads(r.content)['food_inherit_default']])

View File

@@ -402,7 +402,8 @@ class FoodInheritFieldViewSet(viewsets.ReadOnlyModelViewSet):
def get_queryset(self):
# exclude fields not yet implemented
return Food.inherit_fields
self.queryset = Food.inheritable_fields
return super().get_queryset()
class FoodViewSet(viewsets.ModelViewSet, TreeMixin):

View File

@@ -11,6 +11,7 @@ from django.utils.translation import gettext as _
from cookbook.forms import ExportForm, ImportForm, ImportExportBase
from cookbook.helper.permission_helper import group_required
from cookbook.integration.cookbookapp import CookBookApp
from cookbook.integration.copymethat import CopyMeThat
from cookbook.integration.pepperplate import Pepperplate
from cookbook.integration.cheftap import ChefTap
from cookbook.integration.chowdown import Chowdown
@@ -65,6 +66,8 @@ def get_integration(request, export_type):
return Plantoeat(request, export_type)
if export_type == ImportExportBase.COOKBOOKAPP:
return CookBookApp(request, export_type)
if export_type == ImportExportBase.COPYMETHAT:
return CopyMeThat(request, export_type)
@group_required('user')

View File

@@ -201,7 +201,10 @@ class InviteLinkCreate(GroupRequiredMixin, CreateView):
def form_valid(self, form):
obj = form.save(commit=False)
obj.created_by = self.request.user
obj.space = self.request.space
# verify given space is actually owned by the user creating the link
if obj.space.created_by != self.request.user:
obj.space = self.request.space
obj.save()
if obj.email:
try:

View File

@@ -561,7 +561,7 @@ def space(request):
space_form = SpacePreferenceForm(instance=request.space)
space_form.base_fields['food_inherit'].queryset = Food.inherit_fields
space_form.base_fields['food_inherit'].queryset = Food.inheritable_fields
if request.method == "POST" and 'space_form' in request.POST:
form = SpacePreferenceForm(request.POST, prefix='space')
if form.is_valid():