food to tree (model, api, serializer)
This commit is contained in:
parent
6ec0c1c71d
commit
24a575c2d5
@ -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 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):
|
def search_recipes(request, queryset, params):
|
||||||
search_prefs = request.user.searchpreference
|
search_prefs = request.user.searchpreference
|
||||||
search_string = params.get('query', '')
|
search_string = params.get('query', '')
|
||||||
|
57
cookbook/migrations/food_to_tree.py
Normal file
57
cookbook/migrations/food_to_tree.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
@ -48,8 +48,6 @@ class TreeManager(MP_NodeManager):
|
|||||||
|
|
||||||
|
|
||||||
class TreeModel(MP_Node):
|
class TreeModel(MP_Node):
|
||||||
objects = ScopedManager(space='space', _manager_class=TreeManager)
|
|
||||||
|
|
||||||
_full_name_separator = ' > '
|
_full_name_separator = ' > '
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -344,7 +342,9 @@ class Keyword(ExportModelOperationsMixin('keyword'), TreeModel, PermissionModelM
|
|||||||
description = models.TextField(default="", blank=True)
|
description = models.TextField(default="", blank=True)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||||
|
objects = ScopedManager(space='space', _manager_class=TreeManager)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
constraints = [
|
constraints = [
|
||||||
@ -353,7 +353,7 @@ class Keyword(ExportModelOperationsMixin('keyword'), TreeModel, PermissionModelM
|
|||||||
indexes = (Index(fields=['id', 'name']),)
|
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)])
|
name = models.CharField(max_length=128, validators=[MinLengthValidator(1)])
|
||||||
description = models.TextField(blank=True, null=True)
|
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)])
|
name = models.CharField(max_length=128, validators=[MinLengthValidator(1)])
|
||||||
recipe = models.ForeignKey('Recipe', null=True, blank=True, on_delete=models.SET_NULL)
|
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)
|
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)
|
description = models.TextField(default='', blank=True)
|
||||||
|
|
||||||
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||||
objects = ScopedManager(space='space')
|
objects = ScopedManager(space='space', _manager_class=TreeManager)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
@ -289,34 +289,32 @@ class SupermarketSerializer(UniqueFieldsMixin, SpacedModelSerializer):
|
|||||||
class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
|
class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
|
||||||
supermarket_category = SupermarketCategorySerializer(allow_null=True, required=False)
|
supermarket_category = SupermarketCategorySerializer(allow_null=True, required=False)
|
||||||
image = serializers.SerializerMethodField('get_image')
|
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):
|
def get_image(self, obj):
|
||||||
if obj.recipe:
|
if obj.recipe:
|
||||||
recipes = Recipe.objects.filter(id=obj.recipe).exclude(image__isnull=True).exclude(image__exact='')
|
recipes = Recipe.objects.filter(id=obj.recipe).exclude(image__isnull=True).exclude(image__exact='')
|
||||||
if len(recipes) == 0:
|
if len(recipes) == 0:
|
||||||
return recipes.image.url
|
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='')
|
recipes = Recipe.objects.filter(steps__ingredients__food=obj).exclude(image__isnull=True).exclude(image__exact='')
|
||||||
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
|
|
||||||
return None
|
|
||||||
|
|
||||||
# def count_recipes(self, obj):
|
# if no recipes found - check whole tree
|
||||||
# return obj.recipe_set.all().count()
|
if len(recipes) == 0:
|
||||||
|
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 Recipe.objects.filter(steps__ingredients__food=obj).count()
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
validated_data['name'] = validated_data['name'].strip()
|
validated_data['name'] = validated_data['name'].strip()
|
||||||
validated_data['space'] = self.context['request'].space
|
validated_data['space'] = self.context['request'].space
|
||||||
supermarket = validated_data.pop('supermarket_category')
|
|
||||||
obj, created = Food.objects.get_or_create(**validated_data)
|
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
|
return obj
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
@ -325,7 +323,8 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Food
|
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):
|
class IngredientSerializer(WritableNestedModelSerializer):
|
||||||
|
31
cookbook/templates/model/food_template.html
Normal file
31
cookbook/templates/model/food_template.html
Normal 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 %}
|
@ -3,7 +3,7 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load l10n %}
|
{% load l10n %}
|
||||||
|
{% comment %} TODO Can this be combined with Food template? {% endcomment %}
|
||||||
{% block title %}{% trans 'Keywords' %}{% endblock %}
|
{% block title %}{% trans 'Keywords' %}{% endblock %}
|
||||||
|
|
||||||
{% block content_fluid %}
|
{% block content_fluid %}
|
@ -5,7 +5,7 @@ from django.contrib import auth
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from cookbook.models import Keyword, CookLog
|
from cookbook.models import CookLog
|
||||||
|
|
||||||
LIST_URL = 'api:cooklog-list'
|
LIST_URL = 'api:cooklog-list'
|
||||||
DETAIL_URL = 'api:cooklog-detail'
|
DETAIL_URL = 'api:cooklog-detail'
|
||||||
|
@ -4,10 +4,21 @@ import pytest
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django_scopes import scopes_disabled
|
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'
|
LIST_URL = 'api:food-list'
|
||||||
DETAIL_URL = 'api:food-detail'
|
DETAIL_URL = 'api:food-detail'
|
||||||
|
MOVE_URL = 'api:food-move'
|
||||||
|
MERGE_URL = 'api:food-merge'
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
@ -15,11 +26,46 @@ def obj_1(space_1):
|
|||||||
return Food.objects.get_or_create(name='test_1', space=space_1)[0]
|
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
|
@pytest.fixture
|
||||||
def obj_2(space_1):
|
def obj_2(space_1):
|
||||||
return Food.objects.get_or_create(name='test_2', space=space_1)[0]
|
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", [
|
@pytest.mark.parametrize("arg", [
|
||||||
['a_u', 403],
|
['a_u', 403],
|
||||||
['g1_s1', 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):
|
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 json.loads(u1_s1.get(reverse(LIST_URL)).content)['count'] == 2
|
||||||
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0
|
assert json.loads(u1_s2.get(reverse(LIST_URL)).content)['count'] == 0
|
||||||
|
|
||||||
obj_1.space = space_2
|
obj_1.space = space_2
|
||||||
obj_1.save()
|
obj_1.save()
|
||||||
|
|
||||||
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 1
|
assert json.loads(u1_s1.get(reverse(LIST_URL)).content)['count'] == 1
|
||||||
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 1
|
assert json.loads(u1_s2.get(reverse(LIST_URL)).content)['count'] == 1
|
||||||
|
|
||||||
|
|
||||||
def test_list_filter(obj_1, obj_2, u1_s1):
|
def test_list_filter(obj_1, obj_2, u1_s1):
|
||||||
r = u1_s1.get(reverse(LIST_URL))
|
r = u1_s1.get(reverse(LIST_URL))
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
response = json.loads(r.content)
|
response = json.loads(r.content)
|
||||||
assert len(response) == 2
|
assert response['count'] == 2
|
||||||
assert response[0]['name'] == obj_1.name
|
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)
|
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)
|
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)
|
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", [
|
@pytest.mark.parametrize("arg", [
|
||||||
@ -107,7 +159,10 @@ def test_add(arg, request, u1_s2):
|
|||||||
assert r.status_code == 404
|
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(
|
r = u1_s1.post(
|
||||||
reverse(LIST_URL),
|
reverse(LIST_URL),
|
||||||
{'name': obj_1.name},
|
{'name': obj_1.name},
|
||||||
@ -116,6 +171,8 @@ def test_add_duplicate(u1_s1, u1_s2, obj_1):
|
|||||||
response = json.loads(r.content)
|
response = json.loads(r.content)
|
||||||
assert r.status_code == 201
|
assert r.status_code == 201
|
||||||
assert response['id'] == obj_1.id
|
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(
|
r = u1_s2.post(
|
||||||
reverse(LIST_URL),
|
reverse(LIST_URL),
|
||||||
@ -125,9 +182,13 @@ def test_add_duplicate(u1_s1, u1_s2, obj_1):
|
|||||||
response = json.loads(r.content)
|
response = json.loads(r.content)
|
||||||
assert r.status_code == 201
|
assert r.status_code == 201
|
||||||
assert response['id'] != obj_1.id
|
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(
|
r = u1_s2.delete(
|
||||||
reverse(
|
reverse(
|
||||||
DETAIL_URL,
|
DETAIL_URL,
|
||||||
@ -135,14 +196,173 @@ def test_delete(u1_s1, u1_s2, obj_1):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
assert r.status_code == 404
|
assert r.status_code == 404
|
||||||
|
with scopes_disabled():
|
||||||
|
assert Food.objects.count() == 3
|
||||||
|
|
||||||
r = u1_s1.delete(
|
r = u1_s1.delete(
|
||||||
reverse(
|
reverse(
|
||||||
DETAIL_URL,
|
DETAIL_URL,
|
||||||
args={obj_1.id}
|
args={obj_1_1.id}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
assert r.status_code == 204
|
assert r.status_code == 204
|
||||||
with scopes_disabled():
|
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
|
||||||
|
@ -49,28 +49,22 @@ def obj_3(space_2):
|
|||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def recipe_1_s1(obj_1, recipe_1_s1, space_1):
|
def recipe_1_s1(obj_1, recipe_1_s1, space_1):
|
||||||
recipe_1_s1.keywords.add(obj_1.id)
|
return recipe_1_s1.keywords.add(obj_1)
|
||||||
return recipe_1_s1
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def recipe_2_s1(obj_2, recipe_2_s1, space_1):
|
def recipe_2_s1(obj_2, recipe_2_s1, space_1):
|
||||||
recipe_2_s1.keywords.add(obj_2.id)
|
return recipe_2_s1.keywords.add(obj_2)
|
||||||
return recipe_1_s1
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def recipe_3_s2(u1_s2, obj_3, space_2):
|
def recipe_3_s2(u1_s2, obj_3, space_2):
|
||||||
r = get_random_recipe(space_2, u1_s2)
|
return get_random_recipe(space_2, u1_s2).keywords.add(obj_3)
|
||||||
r.keywords.add(obj_3.id)
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def recipe_1_1_s1(u1_s1, obj_1_1, space_1):
|
def recipe_1_1_s1(u1_s1, obj_1_1, space_1):
|
||||||
r = get_random_recipe(space_1, u1_s1)
|
return get_random_recipe(space_1, u1_s1).keywords.add(obj_1_1)
|
||||||
r.keywords.add(obj_1_1.id)
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("arg", [
|
@pytest.mark.parametrize("arg", [
|
||||||
|
@ -6,7 +6,7 @@ from django.forms import model_to_dict
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django_scopes import scopes_disabled
|
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'
|
LIST_URL = 'api:shoppinglistentry-list'
|
||||||
DETAIL_URL = 'api:shoppinglistentry-detail'
|
DETAIL_URL = 'api:shoppinglistentry-detail'
|
||||||
@ -14,7 +14,7 @@ DETAIL_URL = 'api:shoppinglistentry-detail'
|
|||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def obj_1(space_1, u1_s1):
|
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 = ShoppingList.objects.create(created_by=auth.get_user(u1_s1), space=space_1, )
|
||||||
s.entries.add(e)
|
s.entries.add(e)
|
||||||
return e
|
return e
|
||||||
@ -22,7 +22,7 @@ def obj_1(space_1, u1_s1):
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def obj_2(space_1, u1_s1):
|
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 = ShoppingList.objects.create(created_by=auth.get_user(u1_s1), space=space_1, )
|
||||||
s.entries.add(e)
|
s.entries.add(e)
|
||||||
return e
|
return e
|
||||||
|
@ -62,7 +62,7 @@ def get_random_recipe(space_1, u1_s1):
|
|||||||
s1.ingredients.add(
|
s1.ingredients.add(
|
||||||
Ingredient.objects.create(
|
Ingredient.objects.create(
|
||||||
amount=1,
|
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, ),
|
unit=Unit.objects.create(name=uuid.uuid4(), space=space_1, ),
|
||||||
note=uuid.uuid4(),
|
note=uuid.uuid4(),
|
||||||
space=space_1,
|
space=space_1,
|
||||||
@ -72,7 +72,7 @@ def get_random_recipe(space_1, u1_s1):
|
|||||||
s2.ingredients.add(
|
s2.ingredients.add(
|
||||||
Ingredient.objects.create(
|
Ingredient.objects.create(
|
||||||
amount=1,
|
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, ),
|
unit=Unit.objects.create(name=uuid.uuid4(), space=space_1, ),
|
||||||
note=uuid.uuid4(),
|
note=uuid.uuid4(),
|
||||||
space=space_1,
|
space=space_1,
|
||||||
|
@ -87,7 +87,7 @@ urlpatterns = [
|
|||||||
path('edit/recipe/convert/<int:pk>/', edit.convert_recipe, name='edit_convert_recipe'),
|
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/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'),
|
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/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('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/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 deprecated?
|
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'), # TODO is this deprecated?
|
path('dal/unit/', dal.UnitAutocomplete.as_view(), name='dal_unit'),
|
||||||
|
|
||||||
path('telegram/setup/<int:pk>', telegram.setup_bot, name='telegram_setup'),
|
path('telegram/setup/<int:pk>', telegram.setup_bot, name='telegram_setup'),
|
||||||
path('telegram/remove/<int:pk>', telegram.remove_bot, name='telegram_remove'),
|
path('telegram/remove/<int:pk>', telegram.remove_bot, name='telegram_remove'),
|
||||||
@ -135,9 +135,13 @@ urlpatterns = [
|
|||||||
name='web_manifest'),
|
name='web_manifest'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# generic_models = (
|
||||||
|
# Recipe, RecipeImport, Storage, RecipeBook, MealPlan, SyncLog, Sync,
|
||||||
|
# Comment, RecipeBookEntry, Keyword, Food, ShoppingList, InviteLink
|
||||||
|
# )
|
||||||
generic_models = (
|
generic_models = (
|
||||||
Recipe, RecipeImport, Storage, RecipeBook, MealPlan, SyncLog, Sync,
|
Recipe, RecipeImport, Storage, RecipeBook, MealPlan, SyncLog, Sync,
|
||||||
Comment, RecipeBookEntry, Food, ShoppingList, InviteLink
|
Comment, RecipeBookEntry, ShoppingList, InviteLink
|
||||||
)
|
)
|
||||||
|
|
||||||
for m in generic_models:
|
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:
|
for m in tree_models:
|
||||||
py_name = get_model_name(m)
|
py_name = get_model_name(m)
|
||||||
url_name = py_name.replace('_', '-')
|
url_name = py_name.replace('_', '-')
|
||||||
|
@ -12,7 +12,8 @@ from django.contrib.auth.models import User
|
|||||||
from django.contrib.postgres.search import TrigramSimilarity
|
from django.contrib.postgres.search import TrigramSimilarity
|
||||||
from django.core.exceptions import FieldError, ValidationError
|
from django.core.exceptions import FieldError, ValidationError
|
||||||
from django.core.files import File
|
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.http import FileResponse, HttpResponse, JsonResponse
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
from django.shortcuts import redirect, get_object_or_404
|
from django.shortcuts import redirect, get_object_or_404
|
||||||
@ -136,6 +137,7 @@ class FuzzyFilterMixin(ViewSetMixin):
|
|||||||
|
|
||||||
class TreeMixin(FuzzyFilterMixin):
|
class TreeMixin(FuzzyFilterMixin):
|
||||||
model = None
|
model = None
|
||||||
|
related_models = [{'model': None, 'field': None}]
|
||||||
schema = TreeSchema()
|
schema = TreeSchema()
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@ -155,7 +157,7 @@ class TreeMixin(FuzzyFilterMixin):
|
|||||||
elif tree:
|
elif tree:
|
||||||
if tree.isnumeric():
|
if tree.isnumeric():
|
||||||
try:
|
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:
|
except self.model.DoesNotExist:
|
||||||
self.queryset = self.model.objects.none()
|
self.queryset = self.model.objects.none()
|
||||||
else:
|
else:
|
||||||
@ -226,18 +228,24 @@ class TreeMixin(FuzzyFilterMixin):
|
|||||||
if target in source.get_descendants_and_self():
|
if target in source.get_descendants_and_self():
|
||||||
content = {'error': True, 'msg': _('Cannot merge with child object!')}
|
content = {'error': True, 'msg': _('Cannot merge with child object!')}
|
||||||
return Response(content, status=status.HTTP_403_FORBIDDEN)
|
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:
|
for model in self.related_models:
|
||||||
getattr(r, self.basename + 's').add(target)
|
if isinstance(getattr(model['model'], model['field']), ManyToManyDescriptor):
|
||||||
getattr(r, self.basename + 's').remove(source)
|
related = model['model'].objects.filter(**{model['field'] + "__id": source.id}, space=self.request.space)
|
||||||
r.save()
|
else:
|
||||||
children = source.get_children().exclude(id=target.id)
|
related = model['model'].objects.filter(**{model['field']: source}, space=self.request.space)
|
||||||
for c in children:
|
|
||||||
c.move(target, 'sorted-child')
|
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:
|
||||||
|
c.move(target, 'sorted-child')
|
||||||
content = {'msg': _(f'{source.name} was merged successfully with {target.name}')}
|
content = {'msg': _(f'{source.name} was merged successfully with {target.name}')}
|
||||||
source.delete()
|
source.delete()
|
||||||
return Response(content, status=status.HTTP_200_OK)
|
return Response(content, status=status.HTTP_200_OK)
|
||||||
@ -341,6 +349,7 @@ class SupermarketCategoryRelationViewSet(viewsets.ModelViewSet, StandardFilterMi
|
|||||||
class KeywordViewSet(viewsets.ModelViewSet, TreeMixin):
|
class KeywordViewSet(viewsets.ModelViewSet, TreeMixin):
|
||||||
queryset = Keyword.objects
|
queryset = Keyword.objects
|
||||||
model = Keyword
|
model = Keyword
|
||||||
|
related_models = [{'model': Recipe, 'field': 'keywords'}]
|
||||||
serializer_class = KeywordSerializer
|
serializer_class = KeywordSerializer
|
||||||
permission_classes = [CustomIsUser]
|
permission_classes = [CustomIsUser]
|
||||||
pagination_class = DefaultPagination
|
pagination_class = DefaultPagination
|
||||||
@ -356,10 +365,13 @@ class UnitViewSet(viewsets.ModelViewSet, FuzzyFilterMixin):
|
|||||||
return super().get_queryset()
|
return super().get_queryset()
|
||||||
|
|
||||||
|
|
||||||
class FoodViewSet(viewsets.ModelViewSet, FuzzyFilterMixin):
|
class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
|
||||||
queryset = Food.objects
|
queryset = Food.objects
|
||||||
|
model = Food
|
||||||
|
related_models = [{'model': Ingredient, 'field': 'food'}]
|
||||||
serializer_class = FoodSerializer
|
serializer_class = FoodSerializer
|
||||||
permission_classes = [CustomIsUser]
|
permission_classes = [CustomIsUser]
|
||||||
|
pagination_class = DefaultPagination
|
||||||
|
|
||||||
|
|
||||||
class RecipeBookViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
class RecipeBookViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
||||||
|
@ -5,4 +5,9 @@ from cookbook.helper.permission_helper import group_required
|
|||||||
|
|
||||||
@group_required('user')
|
@group_required('user')
|
||||||
def keyword(request):
|
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', {})
|
||||||
|
@ -16,8 +16,8 @@ import re
|
|||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
# from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
# load_dotenv()
|
load_dotenv()
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
# Get vars from .env files
|
# Get vars from .env files
|
||||||
|
Loading…
Reference in New Issue
Block a user