From a7795092b32459c368e0b5051805083a9fc5664b Mon Sep 17 00:00:00 2001 From: smilerz Date: Thu, 14 Sep 2023 14:46:37 -0500 Subject: [PATCH] make 'name' unique in space for MealType make MealType, Unit, Supermarket, Supermarket Category, PropoertyType case insenstive for get_or_create convert unit conversion create serializer to get_or_create behavior enable create duplicate tests for unitconversion and mealtype api --- .../0203_alter_unique_contstraints.py | 17 ++++++ cookbook/models.py | 5 ++ cookbook/serializer.py | 55 ++++++++++++------- cookbook/tests/api/test_api_meal_type.py | 36 ++++++------ .../tests/api/test_api_unit_conversion.py | 50 ++++++++++------- 5 files changed, 106 insertions(+), 57 deletions(-) create mode 100644 cookbook/migrations/0203_alter_unique_contstraints.py diff --git a/cookbook/migrations/0203_alter_unique_contstraints.py b/cookbook/migrations/0203_alter_unique_contstraints.py new file mode 100644 index 00000000..7dc6cc08 --- /dev/null +++ b/cookbook/migrations/0203_alter_unique_contstraints.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.5 on 2023-09-14 12:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cookbook', '0202_remove_space_show_facet_count'), + ] + + operations = [ + migrations.AddConstraint( + model_name='mealtype', + constraint=models.UniqueConstraint(fields=('space', 'name'), name='mt_unique_name_per_space'), + ), + ] diff --git a/cookbook/models.py b/cookbook/models.py index f2869d5f..67dd0d6c 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -982,6 +982,11 @@ class MealType(models.Model, PermissionModelMixin): def __str__(self): return self.name + class Meta: + constraints = [ + models.UniqueConstraint(fields=['space', 'name'], name='mt_unique_name_per_space'), + ] + class MealPlan(ExportModelOperationsMixin('meal_plan'), models.Model, PermissionModelMixin): recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, blank=True, null=True) diff --git a/cookbook/serializer.py b/cookbook/serializer.py index aefa4a83..313e2ec9 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -334,8 +334,11 @@ class SpacedModelSerializer(serializers.ModelSerializer): class MealTypeSerializer(SpacedModelSerializer, WritableNestedModelSerializer): def create(self, validated_data): + validated_data['name'] = validated_data['name'].strip() + space = validated_data.pop('space', self.context['request'].space) validated_data['created_by'] = self.context['request'].user - return super().create(validated_data) + obj, created = MealType.objects.get_or_create(name__iexact=validated_data['name'], space=space, defaults=validated_data) + return obj class Meta: list_serializer_class = SpaceFilterSerializer @@ -456,17 +459,17 @@ class UnitSerializer(UniqueFieldsMixin, ExtendedRecipeMixin, OpenDataModelMixin) recipe_filter = 'steps__ingredients__unit' def create(self, validated_data): - name = validated_data.pop('name').strip() + # get_or_create drops any field that contains '__' when creating so values must be included in validated data + space = validated_data.pop('space', self.context['request'].space) + if x := validated_data.get('name', None): + validated_data['name'] = x.strip() + if x := validated_data.get('name', None): + validated_data['plural_name'] = x.strip() - if plural_name := validated_data.pop('plural_name', None): - plural_name = plural_name.strip() - - if unit := Unit.objects.filter(Q(name=name) | Q(plural_name=name)).first(): + if unit := Unit.objects.filter(Q(name__iexact=validated_data['name']) | Q(plural_name__iexact=validated_data['name']), space=space).first(): return unit - space = validated_data.pop('space', self.context['request'].space) - obj, created = Unit.objects.get_or_create(name=name, plural_name=plural_name, space=space, - defaults=validated_data) + obj, created = Unit.objects.get_or_create(name__iexact=validated_data['name'], space=space, defaults=validated_data) return obj def update(self, instance, validated_data): @@ -484,9 +487,9 @@ class UnitSerializer(UniqueFieldsMixin, ExtendedRecipeMixin, OpenDataModelMixin) class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedModelSerializer, OpenDataModelMixin): def create(self, validated_data): - name = validated_data.pop('name').strip() + validated_data['name'] = validated_data['name'].strip() space = validated_data.pop('space', self.context['request'].space) - obj, created = SupermarketCategory.objects.get_or_create(name=name, space=space) + obj, created = SupermarketCategory.objects.get_or_create(name__iexact=validated_data['name'], space=space, defaults=validated_data) return obj def update(self, instance, validated_data): @@ -508,6 +511,12 @@ class SupermarketCategoryRelationSerializer(WritableNestedModelSerializer): class SupermarketSerializer(UniqueFieldsMixin, SpacedModelSerializer, OpenDataModelMixin): category_to_supermarket = SupermarketCategoryRelationSerializer(many=True, read_only=True) + def create(self, validated_data): + validated_data['name'] = validated_data['name'].strip() + space = validated_data.pop('space', self.context['request'].space) + obj, created = Supermarket.objects.get_or_create(name__iexact=validated_data['name'], space=space, defaults=validated_data) + return obj + class Meta: model = Supermarket fields = ('id', 'name', 'description', 'category_to_supermarket', 'open_data_slug') @@ -517,12 +526,10 @@ class PropertyTypeSerializer(OpenDataModelMixin, WritableNestedModelSerializer, id = serializers.IntegerField(required=False) def create(self, validated_data): - validated_data['space'] = self.context['request'].space - - if property_type := PropertyType.objects.filter(Q(name=validated_data['name'])).filter(space=self.context['request'].space).first(): - return property_type - - return super().create(validated_data) + validated_data['name'] = validated_data['name'].strip() + space = validated_data.pop('space', self.context['request'].space) + obj, created = PropertyType.objects.get_or_create(name__iexact=validated_data['name'], space=space, defaults=validated_data) + return obj class Meta: model = PropertyType @@ -799,9 +806,17 @@ class UnitConversionSerializer(WritableNestedModelSerializer, OpenDataModelMixin return text + f' = {round(obj.converted_amount)} {obj.converted_unit}' def create(self, validated_data): - validated_data['space'] = self.context['request'].space - validated_data['created_by'] = self.context['request'].user - return super().create(validated_data) + validated_data['space'] = validated_data.pop('space', self.context['request'].space) + try: + return UnitConversion.objects.get( + food__name__iexact=validated_data.get('food', {}).get('name', None), + base_unit__name__iexact=validated_data.get('base_unit', {}).get('name', None), + converted_unit__name__iexact=validated_data.get('converted_unit', {}).get('name', None), + space=validated_data['space'] + ) + except UnitConversion.DoesNotExist: + validated_data['created_by'] = self.context['request'].user + return super().create(validated_data) class Meta: model = UnitConversion diff --git a/cookbook/tests/api/test_api_meal_type.py b/cookbook/tests/api/test_api_meal_type.py index de3c73da..e75fd41b 100644 --- a/cookbook/tests/api/test_api_meal_type.py +++ b/cookbook/tests/api/test_api_meal_type.py @@ -90,24 +90,24 @@ def test_add(arg, request, u1_s2): r = u1_s2.get(reverse(DETAIL_URL, args={response['id']})) assert r.status_code == 404 -# TODO make name in space unique -# def test_add_duplicate(u1_s1, u1_s2, obj_1): -# r = u1_s1.post( -# reverse(LIST_URL), -# {'name': obj_1.name}, -# content_type='application/json' -# ) -# response = json.loads(r.content) -# assert r.status_code == 201 -# assert response['id'] == obj_1.id -# -# r = u1_s2.post( -# reverse(LIST_URL), -# {'name': obj_1.name}, -# content_type='application/json' -# ) -# response = json.loads(r.content) -# assert r.status_code == 201 + +def test_add_duplicate(u1_s1, u1_s2, obj_1): + r = u1_s1.post( + reverse(LIST_URL), + {'name': obj_1.name}, + content_type='application/json' + ) + response = json.loads(r.content) + assert r.status_code == 201 + assert response['id'] == obj_1.id + + r = u1_s2.post( + reverse(LIST_URL), + {'name': obj_1.name}, + content_type='application/json' + ) + response = json.loads(r.content) + assert r.status_code == 201 # assert response['id'] != obj_1.id diff --git a/cookbook/tests/api/test_api_unit_conversion.py b/cookbook/tests/api/test_api_unit_conversion.py index a45b6e5d..1484ccdd 100644 --- a/cookbook/tests/api/test_api_unit_conversion.py +++ b/cookbook/tests/api/test_api_unit_conversion.py @@ -121,25 +121,37 @@ def test_add(arg, request, u1_s2, space_1, u1_s1): assert r.status_code == 404 -# TODO make name in space unique -# def test_add_duplicate(u1_s1, u1_s2, obj_1): -# r = u1_s1.post( -# reverse(LIST_URL), -# {'name': obj_1.name}, -# content_type='application/json' -# ) -# response = json.loads(r.content) -# assert r.status_code == 201 -# assert response['id'] == obj_1.id -# -# r = u1_s2.post( -# reverse(LIST_URL), -# {'name': obj_1.name}, -# content_type='application/json' -# ) -# response = json.loads(r.content) -# assert r.status_code == 201 -# assert response['id'] != obj_1.id +def test_add_duplicate(u1_s1, u1_s2, obj_1): + r = u1_s1.post( + reverse(LIST_URL), + { + 'food': {'id': obj_1.food.id, 'name': obj_1.food.name}, + 'base_amount': 100, + 'base_unit': {'id': obj_1.base_unit.id, 'name': obj_1.base_unit.name}, + 'converted_amount': 100, + 'converted_unit': {'id': obj_1.converted_unit.id, 'name': obj_1.converted_unit.name} + }, + + content_type='application/json' + ) + response = json.loads(r.content) + assert r.status_code == 201 + assert response['id'] == obj_1.id + + r = u1_s2.post( + reverse(LIST_URL), + { + 'food': {'id': obj_1.food.id, 'name': obj_1.food.name}, + 'base_amount': 100, + 'base_unit': {'id': obj_1.base_unit.id, 'name': obj_1.base_unit.name}, + 'converted_amount': 100, + 'converted_unit': {'id': obj_1.converted_unit.id, 'name': obj_1.converted_unit.name} + }, + content_type='application/json' + ) + response = json.loads(r.content) + assert r.status_code == 201 + assert response['id'] != obj_1.id def test_delete(u1_s1, u1_s2, obj_1):