Squashed commit of the following:
This commit is contained in:
parent
d1556f69c2
commit
d33a49538e
@ -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)
|
||||||
|
@ -338,11 +338,11 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
|
|||||||
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', None)
|
# supermarket = validated_data.pop('supermarket_category', None)
|
||||||
obj, created = Food.objects.get_or_create(**validated_data)
|
obj, created = Food.objects.get_or_create(**validated_data)
|
||||||
if supermarket:
|
# if supermarket:
|
||||||
obj.supermarket_category, created = SupermarketCategory.objects.get_or_create(name=supermarket['name'], space=self.context['request'].space)
|
# obj.supermarket_category, created = SupermarketCategory.objects.get_or_create(name=supermarket.name, space=self.context['request'].space)
|
||||||
obj.save()
|
# obj.save()
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -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
|
||||||
@ -486,7 +493,7 @@ class RecipePagination(PageNumberPagination):
|
|||||||
max_page_size = 100
|
max_page_size = 100
|
||||||
|
|
||||||
def paginate_queryset(self, queryset, request, view=None):
|
def paginate_queryset(self, queryset, request, view=None):
|
||||||
self.facets = get_facet(queryset, request.query_params, request.space)
|
self.facets = get_facet(queryset, request)
|
||||||
return super().paginate_queryset(queryset, request, view)
|
return super().paginate_queryset(queryset, request, view)
|
||||||
|
|
||||||
def get_paginated_response(self, data):
|
def get_paginated_response(self, data):
|
||||||
|
@ -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')
|
||||||
|
@ -355,7 +355,7 @@ export default {
|
|||||||
|
|
||||||
this.loadMealPlan()
|
this.loadMealPlan()
|
||||||
// this.loadRecentlyViewed()
|
// this.loadRecentlyViewed()
|
||||||
// this.refreshData(false) // this gets triggered when the cookies get loaded
|
this.refreshData(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.$i18n.locale = window.CUSTOM_LOCALE
|
this.$i18n.locale = window.CUSTOM_LOCALE
|
||||||
|
@ -4,12 +4,14 @@
|
|||||||
:options="objects"
|
:options="objects"
|
||||||
:close-on-select="true"
|
:close-on-select="true"
|
||||||
:clear-on-select="true"
|
:clear-on-select="true"
|
||||||
:hide-selected="true"
|
:hide-selected="multiple"
|
||||||
:preserve-search="true"
|
:preserve-search="true"
|
||||||
:placeholder="lookupPlaceholder"
|
:placeholder="lookupPlaceholder"
|
||||||
:label="label"
|
:label="label"
|
||||||
track-by="id"
|
track-by="id"
|
||||||
:multiple="multiple"
|
:multiple="multiple"
|
||||||
|
:taggable="create_new"
|
||||||
|
:tag-placeholder="createText"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@search-change="search"
|
@search-change="search"
|
||||||
@input="selectionChanged">
|
@input="selectionChanged">
|
||||||
@ -41,7 +43,9 @@ export default {
|
|||||||
limit: {type: Number, default: 10,},
|
limit: {type: Number, default: 10,},
|
||||||
sticky_options: {type:Array, default(){return []}},
|
sticky_options: {type:Array, default(){return []}},
|
||||||
initial_selection: {type:Array, default(){return []}},
|
initial_selection: {type:Array, default(){return []}},
|
||||||
multiple: {type: Boolean, default: true}
|
multiple: {type: Boolean, default: true},
|
||||||
|
create_new: {type: Boolean, default: false}, // TODO: this will create option to add new drop-downs
|
||||||
|
create_text: {type: String, default: 'You Forgot to Add a Tag Placeholder'},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
initial_selection: function (newVal, oldVal) { // watch it
|
initial_selection: function (newVal, oldVal) { // watch it
|
||||||
@ -64,6 +68,9 @@ export default {
|
|||||||
lookupPlaceholder() {
|
lookupPlaceholder() {
|
||||||
return this.placeholder || this.model.name || this.$t('Search')
|
return this.placeholder || this.model.name || this.$t('Search')
|
||||||
},
|
},
|
||||||
|
createText() {
|
||||||
|
return this.create_text
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// this.genericAPI inherited from ApiMixin
|
// this.genericAPI inherited from ApiMixin
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
<template v-slot:modal-title><h4>{{form.title}}</h4></template>
|
<template v-slot:modal-title><h4>{{form.title}}</h4></template>
|
||||||
<div v-for="(f, i) in form.fields" v-bind:key=i>
|
<div v-for="(f, i) in form.fields" v-bind:key=i>
|
||||||
<p v-if="f.type=='instruction'">{{f.label}}</p>
|
<p v-if="f.type=='instruction'">{{f.label}}</p>
|
||||||
|
<!-- this lookup is single selection -->
|
||||||
<lookup-input v-if="f.type=='lookup'"
|
<lookup-input v-if="f.type=='lookup'"
|
||||||
:label="f.label"
|
:label="f.label"
|
||||||
:value="f.value"
|
:value="f.value"
|
||||||
@ -12,6 +13,7 @@
|
|||||||
:sticky_options="f.sticky_options || undefined"
|
:sticky_options="f.sticky_options || undefined"
|
||||||
@change="storeValue"/> <!-- TODO add ability to create new items associated with lookup -->
|
@change="storeValue"/> <!-- TODO add ability to create new items associated with lookup -->
|
||||||
<!-- TODO: add emoji field -->
|
<!-- TODO: add emoji field -->
|
||||||
|
<!-- TODO: add multi-selection input list -->
|
||||||
<checkbox-input v-if="f.type=='checkbox'"
|
<checkbox-input v-if="f.type=='checkbox'"
|
||||||
:label="f.label"
|
:label="f.label"
|
||||||
:value="f.value"
|
:value="f.value"
|
||||||
|
@ -4,8 +4,8 @@
|
|||||||
v-bind:label="label"
|
v-bind:label="label"
|
||||||
class="mb-3">
|
class="mb-3">
|
||||||
<generic-multiselect
|
<generic-multiselect
|
||||||
@change="new_value=$event.val['id']"
|
@change="new_value=$event.val"
|
||||||
:initial_selection="[]"
|
:initial_selection="[value]"
|
||||||
:model="model"
|
:model="model"
|
||||||
:multiple="false"
|
:multiple="false"
|
||||||
:sticky_options="sticky_options"
|
:sticky_options="sticky_options"
|
||||||
@ -28,15 +28,15 @@ export default {
|
|||||||
value: {type: Object, default () {return {}}},
|
value: {type: Object, default () {return {}}},
|
||||||
model: {type: Object, default () {return {}}},
|
model: {type: Object, default () {return {}}},
|
||||||
sticky_options: {type:Array, default(){return []}},
|
sticky_options: {type:Array, default(){return []}},
|
||||||
|
// TODO: include create_new and create_text props and associated functionality to create objects for drop down
|
||||||
|
// see 'tagging' here: https://vue-multiselect.js.org/#sub-tagging
|
||||||
|
// perfect world would have it trigger a new modal associated with the associated item model
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
new_value: undefined,
|
new_value: undefined,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
|
||||||
this.new_value = this.value.id
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
modelName() {
|
modelName() {
|
||||||
return this?.model?.name ?? this.$t('Search')
|
return this?.model?.name ?? this.$t('Search')
|
||||||
@ -44,7 +44,7 @@ export default {
|
|||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'new_value': function () {
|
'new_value': function () {
|
||||||
this.$root.$emit('change', this.field, this.new_value)
|
this.$root.$emit('change', this.field, this.new_value?.id ?? null)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -99,7 +99,7 @@ export class Models {
|
|||||||
'type': 'lookup',
|
'type': 'lookup',
|
||||||
'field': 'supermarket_category',
|
'field': 'supermarket_category',
|
||||||
'list': 'SHOPPING_CATEGORY',
|
'list': 'SHOPPING_CATEGORY',
|
||||||
'label': i18n.t('Shopping_Category')
|
'label': i18n.t('Shopping_Category'),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user