food to tree (model, api, serializer)

This commit is contained in:
smilerz 2021-08-15 08:39:45 -05:00
parent 6ec0c1c71d
commit 24a575c2d5
15 changed files with 399 additions and 76 deletions

View File

@ -12,7 +12,6 @@ from cookbook.models import Food, Keyword, Recipe, ViewLog
# TODO create extensive tests to make sure ORs ANDs and various filters, sorting, etc work as expected
# TODO consider creating a simpleListRecipe API that only includes minimum of recipe info and minimal filtering
def search_recipes(request, queryset, params):
search_prefs = request.user.searchpreference
search_string = params.get('query', '')

View File

@ -0,0 +1,57 @@
# Generated by Django 3.2.5 on 2021-08-14 15:40
from treebeard.mp_tree import MP_Node
from django.db import migrations, models
from django_scopes import scopes_disabled
# update if needed
steplen = MP_Node.steplen
alphabet = MP_Node.alphabet
node_order_by = ["name"]
def update_paths(apps, schema_editor):
with scopes_disabled():
Node = apps.get_model("cookbook", "Food")
nodes = Node.objects.all().order_by(*node_order_by)
for i, node in enumerate(nodes, 1):
# for default values, this resolves to: "{:04d}".format(i)
node.path = f"{{:{alphabet[0]}{steplen}d}}".format(i)
if nodes:
Node.objects.bulk_update(nodes, ["path"])
def backwards(apps, schema_editor):
"""nothing to do"""
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0147_auto_20210813_1829'),
]
operations = [
migrations.AddField(
model_name='food',
name='depth',
field=models.PositiveIntegerField(default=0),
preserve_default=False,
),
migrations.AddField(
model_name='food',
name='numchild',
field=models.PositiveIntegerField(default=0),
),
migrations.AddField(
model_name='food',
name='path',
field=models.CharField(default=0, max_length=255, unique=False),
preserve_default=False,
),
migrations.RunPython(update_paths, backwards),
migrations.AlterField(
model_name="food",
name="path",
field=models.CharField(max_length=255, unique=True),
),
]

View File

@ -48,8 +48,6 @@ class TreeManager(MP_NodeManager):
class TreeModel(MP_Node):
objects = ScopedManager(space='space', _manager_class=TreeManager)
_full_name_separator = ' > '
def __str__(self):
@ -344,7 +342,9 @@ class Keyword(ExportModelOperationsMixin('keyword'), TreeModel, PermissionModelM
description = models.TextField(default="", blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space', _manager_class=TreeManager)
class Meta:
constraints = [
@ -353,7 +353,7 @@ class Keyword(ExportModelOperationsMixin('keyword'), TreeModel, PermissionModelM
indexes = (Index(fields=['id', 'name']),)
class Unit(ExportModelOperationsMixin('unit'), TreeModel, PermissionModelMixin):
class Unit(ExportModelOperationsMixin('unit'), models.Model, PermissionModelMixin):
name = models.CharField(max_length=128, validators=[MinLengthValidator(1)])
description = models.TextField(blank=True, null=True)
@ -369,7 +369,9 @@ class Unit(ExportModelOperationsMixin('unit'), TreeModel, PermissionModelMixin):
]
class Food(ExportModelOperationsMixin('food'), models.Model, PermissionModelMixin):
class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
# TODO add find and fix problem functions
node_order_by = ['name']
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)
@ -377,7 +379,7 @@ class Food(ExportModelOperationsMixin('food'), models.Model, PermissionModelMixi
description = models.TextField(default='', blank=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
objects = ScopedManager(space='space', _manager_class=TreeManager)
def __str__(self):
return self.name

View File

@ -289,34 +289,32 @@ class SupermarketSerializer(UniqueFieldsMixin, SpacedModelSerializer):
class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
supermarket_category = SupermarketCategorySerializer(allow_null=True, required=False)
image = serializers.SerializerMethodField('get_image')
#numrecipe = serializers.SerializerMethodField('count_recipes')
numrecipe = serializers.SerializerMethodField('count_recipes')
# TODO check if it is a recipe and get that image first
def get_image(self, obj):
if obj.recipe:
recipes = Recipe.objects.filter(id=obj.recipe).exclude(image__isnull=True).exclude(image__exact='')
if len(recipes) == 0:
return recipes.image.url
# if food is not also a recipe, look for recipe images that use the food
recipes = Recipe.objects.filter(steps__ingredients__food=obj).exclude(image__isnull=True).exclude(image__exact='')
# if no recipes found - check whole tree
if len(recipes) == 0:
recipes = Recipe.objects.filter(keywords__in=obj.get_tree()).exclude(image__isnull=True).exclude(image__exact='') # if no recipes found - check whole tree
# if len(recipes) != 0:
# return random.choice(recipes).image.url
# else:
# return None
recipes = Recipe.objects.filter(steps__ingredients__food__in=obj.get_tree()).exclude(image__isnull=True).exclude(image__exact='')
if len(recipes) != 0:
return random.choice(recipes).image.url
else:
return None
# def count_recipes(self, obj):
# return obj.recipe_set.all().count()
def count_recipes(self, obj):
return Recipe.objects.filter(steps__ingredients__food=obj).count()
def create(self, validated_data):
validated_data['name'] = validated_data['name'].strip()
validated_data['space'] = self.context['request'].space
supermarket = validated_data.pop('supermarket_category')
obj, created = Food.objects.get_or_create(**validated_data)
if supermarket:
obj.supermarket_category, created = SupermarketCategory.objects.get_or_create(name=supermarket['name'], space=self.context['request'].space)
obj.save()
return obj
def update(self, instance, validated_data):
@ -325,7 +323,8 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
class Meta:
model = Food
fields = ('id', 'name', 'recipe', 'ignore_shopping', 'supermarket_category', 'image')
fields = ('id', 'name', 'recipe', 'ignore_shopping', 'supermarket_category', 'image', 'parent', 'numchild', 'numrecipe')
read_only_fields = ('id', 'numchild', 'parent', 'image')
class IngredientSerializer(WritableNestedModelSerializer):

View File

@ -0,0 +1,31 @@
{% extends "base.html" %}
{% load render_bundle from webpack_loader %}
{% load static %}
{% load i18n %}
{% load l10n %}
{% comment %} TODO Can this be combined with Keyword template? {% endcomment %}
{% block title %}{% trans 'Food' %}{% endblock %}
{% block content_fluid %}
<div id="app" >
<food-list-view></food-list-view>
</div>
{% endblock %}
{% block script %}
{% if debug %}
<script src="{% url 'js_reverse' %}"></script>
{% else %}
<script src="{% static 'django_js_reverse/reverse.js' %}"></script>
{% endif %}
<script type="application/javascript">
window.IMAGE_PLACEHOLDER = "{% static 'assets/recipe_no_image.svg' %}"
</script>
{% render_bundle 'food_list_view' %}
{% endblock %}

View File

@ -3,7 +3,7 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% comment %} TODO Can this be combined with Food template? {% endcomment %}
{% block title %}{% trans 'Keywords' %}{% endblock %}
{% block content_fluid %}

View File

@ -5,7 +5,7 @@ from django.contrib import auth
from django.urls import reverse
from django_scopes import scopes_disabled
from cookbook.models import Keyword, CookLog
from cookbook.models import CookLog
LIST_URL = 'api:cooklog-list'
DETAIL_URL = 'api:cooklog-detail'

View File

@ -4,10 +4,21 @@ import pytest
from django.urls import reverse
from django_scopes import scopes_disabled
from cookbook.models import Food
from cookbook.models import Food, Ingredient
# ------------------ IMPORTANT -------------------
#
# if changing any capabilities associated with food
# you will need to ensure that it is tested against both
# SqlLite and PostgresSQL
# adding load_env() to settings.py will enable Postgress access
#
# ------------------ IMPORTANT -------------------
LIST_URL = 'api:food-list'
DETAIL_URL = 'api:food-detail'
MOVE_URL = 'api:food-move'
MERGE_URL = 'api:food-merge'
@pytest.fixture()
@ -15,11 +26,46 @@ def obj_1(space_1):
return Food.objects.get_or_create(name='test_1', space=space_1)[0]
@pytest.fixture()
def obj_1_1(obj_1, space_1):
return obj_1.add_child(name='test_1_1', space=space_1)
@pytest.fixture()
def obj_1_1_1(obj_1_1, space_1):
return obj_1_1.add_child(name='test_1_1_1', space=space_1)
@pytest.fixture
def obj_2(space_1):
return Food.objects.get_or_create(name='test_2', space=space_1)[0]
@pytest.fixture()
def obj_3(space_2):
return Food.objects.get_or_create(name='test_3', space=space_2)[0]
@pytest.fixture()
def step_1_s1(obj_1, space_1):
return Ingredient.objects.create(food=obj_1, space=space_1)
@pytest.fixture()
def step_2_s1(obj_2, space_1):
return Ingredient.objects.create(food=obj_2, space=space_1)
@pytest.fixture()
def step_3_s2(obj_3, space_2):
return Ingredient.objects.create(food=obj_3, space=space_2)
@pytest.fixture()
def step_1_1_s1(obj_1_1, space_1):
return Ingredient.objects.create(food=obj_1_1, space=space_1)
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
@ -32,31 +78,37 @@ def test_list_permission(arg, request):
def test_list_space(obj_1, obj_2, u1_s1, u1_s2, space_2):
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 2
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0
assert json.loads(u1_s1.get(reverse(LIST_URL)).content)['count'] == 2
assert json.loads(u1_s2.get(reverse(LIST_URL)).content)['count'] == 0
obj_1.space = space_2
obj_1.save()
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 1
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 1
assert json.loads(u1_s1.get(reverse(LIST_URL)).content)['count'] == 1
assert json.loads(u1_s2.get(reverse(LIST_URL)).content)['count'] == 1
def test_list_filter(obj_1, obj_2, u1_s1):
r = u1_s1.get(reverse(LIST_URL))
assert r.status_code == 200
response = json.loads(r.content)
assert len(response) == 2
assert response[0]['name'] == obj_1.name
assert response['count'] == 2
assert response['results'][0]['name'] == obj_1.name
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?page_size=1').content)
assert len(response['results']) == 1
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?limit=1').content)
assert len(response) == 1
assert len(response['results']) == 1
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query=''&limit=1').content)
assert len(response['results']) == 1
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query=chicken').content)
assert len(response) == 0
assert response['count'] == 0
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query={obj_1.name[4:]}').content)
assert len(response) == 1
assert response['count'] == 1
@pytest.mark.parametrize("arg", [
@ -107,7 +159,10 @@ def test_add(arg, request, u1_s2):
assert r.status_code == 404
def test_add_duplicate(u1_s1, u1_s2, obj_1):
@pytest.mark.django_db(transaction=True)
def test_add_duplicate(u1_s1, u1_s2, obj_1, obj_3):
assert json.loads(u1_s1.get(reverse(LIST_URL)).content)['count'] == 1
assert json.loads(u1_s2.get(reverse(LIST_URL)).content)['count'] == 1
r = u1_s1.post(
reverse(LIST_URL),
{'name': obj_1.name},
@ -116,6 +171,8 @@ def test_add_duplicate(u1_s1, u1_s2, obj_1):
response = json.loads(r.content)
assert r.status_code == 201
assert response['id'] == obj_1.id
assert response['name'] == obj_1.name
assert json.loads(u1_s1.get(reverse(LIST_URL)).content)['count'] == 1
r = u1_s2.post(
reverse(LIST_URL),
@ -125,9 +182,13 @@ def test_add_duplicate(u1_s1, u1_s2, obj_1):
response = json.loads(r.content)
assert r.status_code == 201
assert response['id'] != obj_1.id
assert json.loads(u1_s2.get(reverse(LIST_URL)).content)['count'] == 2
def test_delete(u1_s1, u1_s2, obj_1):
def test_delete(u1_s1, u1_s2, obj_1, obj_1_1, obj_1_1_1):
with scopes_disabled():
assert Food.objects.count() == 3
r = u1_s2.delete(
reverse(
DETAIL_URL,
@ -135,14 +196,173 @@ def test_delete(u1_s1, u1_s2, obj_1):
)
)
assert r.status_code == 404
with scopes_disabled():
assert Food.objects.count() == 3
r = u1_s1.delete(
reverse(
DETAIL_URL,
args={obj_1.id}
args={obj_1_1.id}
)
)
assert r.status_code == 204
with scopes_disabled():
assert Food.objects.count() == 0
assert Food.objects.count() == 1
assert Food.find_problems() == ([], [], [], [], [])
def test_move(u1_s1, obj_1, obj_1_1, obj_1_1_1, obj_2, obj_3, space_1):
url = reverse(MOVE_URL, args=[obj_1_1.id, obj_2.id])
with scopes_disabled():
assert obj_1.get_num_children() == 1
assert obj_1.get_descendant_count() == 2
assert Food.get_root_nodes().filter(space=space_1).count() == 2
# move child to new parent, only HTTP put method should work
r = u1_s1.get(url)
assert r.status_code == 405
r = u1_s1.post(url)
assert r.status_code == 405
r = u1_s1.delete(url)
assert r.status_code == 405
r = u1_s1.put(url)
assert r.status_code == 200
with scopes_disabled():
# django-treebeard bypasses django ORM so object needs retrieved again
obj_1 = Food.objects.get(pk=obj_1.id)
obj_2 = Food.objects.get(pk=obj_2.id)
assert obj_1.get_num_children() == 0
assert obj_1.get_descendant_count() == 0
assert obj_2.get_num_children() == 1
assert obj_2.get_descendant_count() == 2
# move child to root
r = u1_s1.put(reverse(MOVE_URL, args=[obj_1_1.id, 0]))
assert r.status_code == 200
with scopes_disabled():
assert Food.get_root_nodes().filter(space=space_1).count() == 3
# attempt to move to non-existent parent
r = u1_s1.put(
reverse(MOVE_URL, args=[obj_1.id, 9999])
)
assert r.status_code == 400
# attempt to move to wrong space
r = u1_s1.put(
reverse(MOVE_URL, args=[obj_1_1.id, obj_3.id])
)
assert r.status_code == 400
# run diagnostic to find problems - none should be found
with scopes_disabled():
assert Food.find_problems() == ([], [], [], [], [])
# this seems overly long - should it be broken into smaller pieces?
def test_merge(
u1_s1,
obj_1, obj_1_1, obj_1_1_1, obj_2, obj_3,
step_1_s1, step_2_s1, step_3_s2, step_1_1_s1,
space_1
):
with scopes_disabled():
assert obj_1.get_num_children() == 1
assert obj_1.get_descendant_count() == 2
assert Food.get_root_nodes().filter(space=space_1).count() == 2
assert Food.objects.filter(space=space_1).count() == 4
assert obj_1.ingredient_set.count() == 1
assert obj_2.ingredient_set.count() == 1
assert obj_3.ingredient_set.count() == 1
assert obj_1_1.ingredient_set.count() == 1
assert obj_1_1_1.ingredient_set.count() == 0
# merge food with no children and no ingredient with another food, only HTTP put method should work
url = reverse(MERGE_URL, args=[obj_1_1_1.id, obj_2.id])
r = u1_s1.get(url)
assert r.status_code == 405
r = u1_s1.post(url)
assert r.status_code == 405
r = u1_s1.delete(url)
assert r.status_code == 405
r = u1_s1.put(url)
assert r.status_code == 200
with scopes_disabled():
# django-treebeard bypasses django ORM so object needs retrieved again
obj_1 = Food.objects.get(pk=obj_1.id)
obj_2 = Food.objects.get(pk=obj_2.id)
assert Food.objects.filter(pk=obj_1_1_1.id).count() == 0
assert obj_1.get_num_children() == 1
assert obj_1.get_descendant_count() == 1
assert obj_2.get_num_children() == 0
assert obj_2.get_descendant_count() == 0
assert obj_1.ingredient_set.count() == 1
assert obj_2.ingredient_set.count() == 1
assert obj_3.ingredient_set.count() == 1
assert obj_1_1.ingredient_set.count() == 1
# merge food with children to another food
r = u1_s1.put(reverse(MERGE_URL, args=[obj_1.id, obj_2.id]))
assert r.status_code == 200
with scopes_disabled():
# django-treebeard bypasses django ORM so object needs retrieved again
obj_2 = Food.objects.get(pk=obj_2.id)
assert Food.objects.filter(pk=obj_1.id).count() == 0
assert obj_2.get_num_children() == 1
assert obj_2.get_descendant_count() == 1
assert obj_2.ingredient_set.count() == 2
assert obj_3.ingredient_set.count() == 1
assert obj_1_1.ingredient_set.count() == 1
# attempt to merge with non-existent parent
r = u1_s1.put(
reverse(MERGE_URL, args=[obj_1_1.id, 9999])
)
assert r.status_code == 400
# attempt to move to wrong space
r = u1_s1.put(
reverse(MERGE_URL, args=[obj_2.id, obj_3.id])
)
assert r.status_code == 400
# attempt to merge with child
r = u1_s1.put(
reverse(MERGE_URL, args=[obj_2.id, obj_1_1.id])
)
assert r.status_code == 403
# attempt to merge with self
r = u1_s1.put(
reverse(MERGE_URL, args=[obj_2.id, obj_2.id])
)
assert r.status_code == 403
# run diagnostic to find problems - none should be found
with scopes_disabled():
assert Food.find_problems() == ([], [], [], [], [])
def test_root_filter(obj_1, obj_1_1, obj_1_1_1, obj_2, obj_3, u1_s1):
# should return root objects in the space (obj_1, obj_2), ignoring query filters
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?root=0').content)
assert len(response['results']) == 2
with scopes_disabled():
obj_2.move(obj_1, 'sorted-child')
# should return direct children of obj_1 (obj_1_1, obj_2), ignoring query filters
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?root={obj_1.id}').content)
assert response['count'] == 2
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?root={obj_1.id}&query={obj_2.name[4:]}').content)
assert response['count'] == 2
def test_tree_filter(obj_1, obj_1_1, obj_1_1_1, obj_2, obj_3, u1_s1):
with scopes_disabled():
obj_2.move(obj_1, 'sorted-child')
# should return full tree starting at obj_1 (obj_1_1_1, obj_2), ignoring query filters
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?tree={obj_1.id}').content)
assert response['count'] == 4
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?tree={obj_1.id}&query={obj_2.name[4:]}').content)
assert response['count'] == 4

View File

@ -49,28 +49,22 @@ def obj_3(space_2):
@pytest.fixture()
def recipe_1_s1(obj_1, recipe_1_s1, space_1):
recipe_1_s1.keywords.add(obj_1.id)
return recipe_1_s1
return recipe_1_s1.keywords.add(obj_1)
@pytest.fixture()
def recipe_2_s1(obj_2, recipe_2_s1, space_1):
recipe_2_s1.keywords.add(obj_2.id)
return recipe_1_s1
return recipe_2_s1.keywords.add(obj_2)
@pytest.fixture()
def recipe_3_s2(u1_s2, obj_3, space_2):
r = get_random_recipe(space_2, u1_s2)
r.keywords.add(obj_3.id)
return r
return get_random_recipe(space_2, u1_s2).keywords.add(obj_3)
@pytest.fixture()
def recipe_1_1_s1(u1_s1, obj_1_1, space_1):
r = get_random_recipe(space_1, u1_s1)
r.keywords.add(obj_1_1.id)
return r
return get_random_recipe(space_1, u1_s1).keywords.add(obj_1_1)
@pytest.mark.parametrize("arg", [

View File

@ -6,7 +6,7 @@ from django.forms import model_to_dict
from django.urls import reverse
from django_scopes import scopes_disabled
from cookbook.models import RecipeBook, Storage, Sync, SyncLog, ShoppingList, ShoppingListEntry, Food
from cookbook.models import ShoppingList, ShoppingListEntry, Food
LIST_URL = 'api:shoppinglistentry-list'
DETAIL_URL = 'api:shoppinglistentry-detail'
@ -14,7 +14,7 @@ DETAIL_URL = 'api:shoppinglistentry-detail'
@pytest.fixture()
def obj_1(space_1, u1_s1):
e = ShoppingListEntry.objects.create(food=Food.objects.create(name='test 1', space=space_1))
e = ShoppingListEntry.objects.create(food=Food.objects.get_or_create(name='test 1', space=space_1)[0])
s = ShoppingList.objects.create(created_by=auth.get_user(u1_s1), space=space_1, )
s.entries.add(e)
return e
@ -22,7 +22,7 @@ def obj_1(space_1, u1_s1):
@pytest.fixture
def obj_2(space_1, u1_s1):
e = ShoppingListEntry.objects.create(food=Food.objects.create(name='test 2', space=space_1))
e = ShoppingListEntry.objects.create(food=Food.objects.get_or_create(name='test 2', space=space_1)[0])
s = ShoppingList.objects.create(created_by=auth.get_user(u1_s1), space=space_1, )
s.entries.add(e)
return e

View File

@ -62,7 +62,7 @@ def get_random_recipe(space_1, u1_s1):
s1.ingredients.add(
Ingredient.objects.create(
amount=1,
food=Food.objects.create(name=uuid.uuid4(), space=space_1, ),
food=Food.objects.get_or_create(name=str(uuid.uuid4()), space=space_1)[0],
unit=Unit.objects.create(name=uuid.uuid4(), space=space_1, ),
note=uuid.uuid4(),
space=space_1,
@ -72,7 +72,7 @@ def get_random_recipe(space_1, u1_s1):
s2.ingredients.add(
Ingredient.objects.create(
amount=1,
food=Food.objects.create(name=uuid.uuid4(), space=space_1, ),
food=Food.objects.get_or_create(name=str(uuid.uuid4()), space=space_1)[0],
unit=Unit.objects.create(name=uuid.uuid4(), space=space_1, ),
note=uuid.uuid4(),
space=space_1,

View File

@ -87,7 +87,7 @@ urlpatterns = [
path('edit/recipe/convert/<int:pk>/', edit.convert_recipe, name='edit_convert_recipe'),
path('edit/storage/<int:pk>/', edit.edit_storage, name='edit_storage'),
path('edit/ingredient/', edit.edit_ingredients, name='edit_food'),
path('edit/ingredient/', edit.edit_ingredients, name='edit_food'), # TODO is this still needed?
path('delete/recipe-source/<int:pk>/', delete.delete_recipe_source, name='delete_recipe_source'),
@ -109,9 +109,9 @@ urlpatterns = [
path('api/ingredient-from-string/', api.ingredient_from_string, name='api_ingredient_from_string'),
path('api/share-link/<int:pk>', api.share_link, name='api_share_link'),
path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'), # TODO is this deprecated?
path('dal/food/', dal.IngredientsAutocomplete.as_view(), name='dal_food'), # TODO is this deprecated?
path('dal/unit/', dal.UnitAutocomplete.as_view(), name='dal_unit'), # TODO is this deprecated?
path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'), # TODO is this still needed?
path('dal/food/', dal.IngredientsAutocomplete.as_view(), name='dal_food'), # TODO is this still needed?
path('dal/unit/', dal.UnitAutocomplete.as_view(), name='dal_unit'),
path('telegram/setup/<int:pk>', telegram.setup_bot, name='telegram_setup'),
path('telegram/remove/<int:pk>', telegram.remove_bot, name='telegram_remove'),
@ -135,9 +135,13 @@ urlpatterns = [
name='web_manifest'),
]
# generic_models = (
# Recipe, RecipeImport, Storage, RecipeBook, MealPlan, SyncLog, Sync,
# Comment, RecipeBookEntry, Keyword, Food, ShoppingList, InviteLink
# )
generic_models = (
Recipe, RecipeImport, Storage, RecipeBook, MealPlan, SyncLog, Sync,
Comment, RecipeBookEntry, Food, ShoppingList, InviteLink
Comment, RecipeBookEntry, ShoppingList, InviteLink
)
for m in generic_models:
@ -176,7 +180,7 @@ for m in generic_models:
)
)
tree_models = [Keyword]
tree_models = [Keyword, Food]
for m in tree_models:
py_name = get_model_name(m)
url_name = py_name.replace('_', '-')

View File

@ -12,7 +12,8 @@ from django.contrib.auth.models import User
from django.contrib.postgres.search import TrigramSimilarity
from django.core.exceptions import FieldError, ValidationError
from django.core.files import File
from django.db.models import Case, Q, Value, When
from django.db.models import ManyToManyField, Q
from django.db.models.fields.related_descriptors import ManyToManyDescriptor
from django.http import FileResponse, HttpResponse, JsonResponse
from django_scopes import scopes_disabled
from django.shortcuts import redirect, get_object_or_404
@ -136,6 +137,7 @@ class FuzzyFilterMixin(ViewSetMixin):
class TreeMixin(FuzzyFilterMixin):
model = None
related_models = [{'model': None, 'field': None}]
schema = TreeSchema()
def get_queryset(self):
@ -155,7 +157,7 @@ class TreeMixin(FuzzyFilterMixin):
elif tree:
if tree.isnumeric():
try:
self.queryset = self.model.objects.get(id=int(tree)).get_descendants_and_self()
self.queryset = self.model.objects.get(id=int(tree)).get_descendants_and_self().filter(space=self.request.space)
except self.model.DoesNotExist:
self.queryset = self.model.objects.none()
else:
@ -226,14 +228,20 @@ class TreeMixin(FuzzyFilterMixin):
if target in source.get_descendants_and_self():
content = {'error': True, 'msg': _('Cannot merge with child object!')}
return Response(content, status=status.HTTP_403_FORBIDDEN)
########################################################################
# TODO this needs abstracted to update steps instead of recipes for food merge
########################################################################
recipes = Recipe.objects.filter(**{"%ss" % self.basename: source}, space=self.request.space)
for r in recipes:
getattr(r, self.basename + 's').add(target)
getattr(r, self.basename + 's').remove(source)
for model in self.related_models:
if isinstance(getattr(model['model'], model['field']), ManyToManyDescriptor):
related = model['model'].objects.filter(**{model['field'] + "__id": source.id}, space=self.request.space)
else:
related = model['model'].objects.filter(**{model['field']: source}, space=self.request.space)
for r in related:
try:
getattr(r, model['field']).add(target)
getattr(r, model['field']).remove(source)
r.save()
except AttributeError:
setattr(r, model['field'], target)
r.save()
children = source.get_children().exclude(id=target.id)
for c in children:
@ -341,6 +349,7 @@ class SupermarketCategoryRelationViewSet(viewsets.ModelViewSet, StandardFilterMi
class KeywordViewSet(viewsets.ModelViewSet, TreeMixin):
queryset = Keyword.objects
model = Keyword
related_models = [{'model': Recipe, 'field': 'keywords'}]
serializer_class = KeywordSerializer
permission_classes = [CustomIsUser]
pagination_class = DefaultPagination
@ -356,10 +365,13 @@ class UnitViewSet(viewsets.ModelViewSet, FuzzyFilterMixin):
return super().get_queryset()
class FoodViewSet(viewsets.ModelViewSet, FuzzyFilterMixin):
class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
queryset = Food.objects
model = Food
related_models = [{'model': Ingredient, 'field': 'food'}]
serializer_class = FoodSerializer
permission_classes = [CustomIsUser]
pagination_class = DefaultPagination
class RecipeBookViewSet(viewsets.ModelViewSet, StandardFilterMixin):

View File

@ -5,4 +5,9 @@ from cookbook.helper.permission_helper import group_required
@group_required('user')
def keyword(request):
return render(request, 'generic/tree_template.html', {})
return render(request, 'model/keyword_template.html', {})
@group_required('user')
def food(request):
return render(request, 'model/food_template.html', {})

View File

@ -16,8 +16,8 @@ import re
from django.contrib import messages
from django.utils.translation import gettext_lazy as _
# from dotenv import load_dotenv
# load_dotenv()
from dotenv import load_dotenv
load_dotenv()
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Get vars from .env files