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.validators import MinLengthValidator
|
||||
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.translation import gettext as _
|
||||
from treebeard.mp_tree import MP_Node, MP_NodeManager
|
||||
@ -385,6 +385,12 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
|
||||
def __str__(self):
|
||||
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:
|
||||
constraints = [
|
||||
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):
|
||||
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)
|
||||
amount = models.DecimalField(default=0, decimal_places=16, max_digits=32)
|
||||
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() == ([], [], [], [], [])
|
||||
|
||||
|
||||
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):
|
||||
url = reverse(MOVE_URL, args=[obj_1_1.id, obj_2.id])
|
||||
with scopes_disabled():
|
||||
|
@ -12,7 +12,7 @@ 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 Case, ProtectedError, Q, Value, When
|
||||
from django.db.models.fields.related import ForeignObjectRel
|
||||
from django.http import FileResponse, HttpResponse, JsonResponse
|
||||
from django_scopes import scopes_disabled
|
||||
@ -380,6 +380,13 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
|
||||
permission_classes = [CustomIsUser]
|
||||
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):
|
||||
queryset = RecipeBook.objects
|
||||
|
@ -16,6 +16,7 @@
|
||||
## Coming Next
|
||||
- Heirarchical Ingredients
|
||||
- Faceted Search
|
||||
- Search filter by rating
|
||||
- What Can I Make Now?
|
||||
- Better ingredient/unit matching on import
|
||||
- Custom word replacement on import (e.g. 'grams' automatically imported as 'g')
|
||||
|
Loading…
Reference in New Issue
Block a user