allow deleting of Food part of Ingredient, but not recipe
This commit is contained in:
parent
cd0259ad26
commit
ab316236f9
@ -12,7 +12,7 @@ from django.contrib.postgres.search import SearchVectorField
|
|||||||
from django.core.files.uploadedfile import UploadedFile, InMemoryUploadedFile
|
from django.core.files.uploadedfile import UploadedFile, InMemoryUploadedFile
|
||||||
from django.core.validators import MinLengthValidator
|
from django.core.validators import MinLengthValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Index
|
from django.db.models import Index, ProtectedError
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from treebeard.mp_tree import MP_Node, MP_NodeManager
|
from treebeard.mp_tree import MP_Node, MP_NodeManager
|
||||||
@ -385,6 +385,12 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
if len(self.ingredient_set.all().exclude(step=None)) > 0:
|
||||||
|
raise ProtectedError(self.name + _(" is part of a recipe step and cannot be deleted"), self.ingredient_set.all().exclude(step=None))
|
||||||
|
else:
|
||||||
|
return super().delete()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
constraints = [
|
constraints = [
|
||||||
models.UniqueConstraint(fields=['space', 'name'], name='f_unique_name_per_space')
|
models.UniqueConstraint(fields=['space', 'name'], name='f_unique_name_per_space')
|
||||||
@ -393,7 +399,8 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
|
|||||||
|
|
||||||
|
|
||||||
class Ingredient(ExportModelOperationsMixin('ingredient'), models.Model, PermissionModelMixin):
|
class Ingredient(ExportModelOperationsMixin('ingredient'), models.Model, PermissionModelMixin):
|
||||||
food = models.ForeignKey(Food, on_delete=models.PROTECT, null=True, blank=True)
|
# a pre-delete signal on Food checks if the Ingredient is part of a Step, if it is raises a ProtectedError instead of cascading the delete
|
||||||
|
food = models.ForeignKey(Food, on_delete=models.CASCADE, null=True, blank=True)
|
||||||
unit = models.ForeignKey(Unit, on_delete=models.PROTECT, null=True, blank=True)
|
unit = models.ForeignKey(Unit, on_delete=models.PROTECT, null=True, blank=True)
|
||||||
amount = models.DecimalField(default=0, decimal_places=16, max_digits=32)
|
amount = models.DecimalField(default=0, decimal_places=16, max_digits=32)
|
||||||
note = models.CharField(max_length=256, null=True, blank=True)
|
note = models.CharField(max_length=256, null=True, blank=True)
|
||||||
|
@ -243,6 +243,42 @@ def test_delete(u1_s1, u1_s2, obj_1, obj_1_1, obj_1_1_1):
|
|||||||
assert Food.find_problems() == ([], [], [], [], [])
|
assert Food.find_problems() == ([], [], [], [], [])
|
||||||
|
|
||||||
|
|
||||||
|
def test_integrity(u1_s1, recipe_1_s1):
|
||||||
|
with scopes_disabled():
|
||||||
|
assert Food.objects.count() == 10
|
||||||
|
assert Ingredient.objects.count() == 10
|
||||||
|
f_1 = Food.objects.first()
|
||||||
|
|
||||||
|
# deleting food will fail because food is part of recipe
|
||||||
|
r = u1_s1.delete(
|
||||||
|
reverse(
|
||||||
|
DETAIL_URL,
|
||||||
|
args={f_1.id}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert r.status_code == 403
|
||||||
|
|
||||||
|
with scopes_disabled():
|
||||||
|
i_1 = f_1.ingredient_set.first()
|
||||||
|
# remove Ingredient that references Food from recipe step
|
||||||
|
i_1.step_set.first().ingredients.remove(i_1)
|
||||||
|
assert Food.objects.count() == 10
|
||||||
|
assert Ingredient.objects.count() == 10
|
||||||
|
|
||||||
|
# deleting food will succeed because its not part of recipe and delete will cascade to Ingredient
|
||||||
|
r = u1_s1.delete(
|
||||||
|
reverse(
|
||||||
|
DETAIL_URL,
|
||||||
|
args={f_1.id}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert r.status_code == 204
|
||||||
|
|
||||||
|
with scopes_disabled():
|
||||||
|
assert Food.objects.count() == 9
|
||||||
|
assert Ingredient.objects.count() == 9
|
||||||
|
|
||||||
|
|
||||||
def test_move(u1_s1, obj_1, obj_1_1, obj_1_1_1, obj_2, obj_3, space_1):
|
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])
|
url = reverse(MOVE_URL, args=[obj_1_1.id, obj_2.id])
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
|
@ -12,7 +12,7 @@ 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 Case, ProtectedError, Q, Value, When
|
||||||
from django.db.models.fields.related import ForeignObjectRel
|
from django.db.models.fields.related import ForeignObjectRel
|
||||||
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
|
||||||
@ -380,6 +380,13 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
|
|||||||
permission_classes = [CustomIsUser]
|
permission_classes = [CustomIsUser]
|
||||||
pagination_class = DefaultPagination
|
pagination_class = DefaultPagination
|
||||||
|
|
||||||
|
def destroy(self, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
return (super().destroy(self, *args, **kwargs))
|
||||||
|
except ProtectedError as e:
|
||||||
|
content = {'error': True, 'msg': e.args[0]}
|
||||||
|
return Response(content, status=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
|
||||||
class RecipeBookViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
class RecipeBookViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
||||||
queryset = RecipeBook.objects
|
queryset = RecipeBook.objects
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
## Coming Next
|
## Coming Next
|
||||||
- Heirarchical Ingredients
|
- Heirarchical Ingredients
|
||||||
- Faceted Search
|
- Faceted Search
|
||||||
|
- Search filter by rating
|
||||||
- What Can I Make Now?
|
- What Can I Make Now?
|
||||||
- Better ingredient/unit matching on import
|
- Better ingredient/unit matching on import
|
||||||
- Custom word replacement on import (e.g. 'grams' automatically imported as 'g')
|
- Custom word replacement on import (e.g. 'grams' automatically imported as 'g')
|
||||||
|
Loading…
Reference in New Issue
Block a user