diff --git a/cookbook/serializer.py b/cookbook/serializer.py index 08133f6f8..15dc9e140 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -106,6 +106,8 @@ class CustomOnHandField(serializers.Field): return instance def to_representation(self, obj): + if not self.context["request"].user.is_authenticated: + return [] shared_users = [] if c := caches['default'].get( f'shopping_shared_users_{self.context["request"].space.id}_{self.context["request"].user.id}', None): @@ -540,6 +542,8 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR images = ['recipe__image'] def get_substitute_onhand(self, obj): + if not self.context["request"].user.is_authenticated: + return [] shared_users = [] if c := caches['default'].get( f'shopping_shared_users_{self.context["request"].space.id}_{self.context["request"].user.id}', None): @@ -750,6 +754,10 @@ class UnitConversionSerializer(WritableNestedModelSerializer): class PropertyTypeSerializer(serializers.ModelSerializer): def create(self, validated_data): validated_data['space'] = self.context['request'].space + + if property_type := PropertyType.objects.filter(Q(name=validated_data['name']) ).first(): + return property_type + return super().create(validated_data) class Meta: diff --git a/cookbook/tests/api/test_api_food_property.py b/cookbook/tests/api/test_api_food_property.py new file mode 100644 index 000000000..b27f6a0b5 --- /dev/null +++ b/cookbook/tests/api/test_api_food_property.py @@ -0,0 +1,158 @@ +import json +from datetime import datetime, timedelta + +import pytest +from django.contrib import auth +from django.urls import reverse +from django_scopes import scope, scopes_disabled + +from cookbook.models import Food, MealPlan, MealType +from cookbook.tests.factories import RecipeFactory + +LIST_URL = 'api:mealplan-list' +DETAIL_URL = 'api:mealplan-detail' + +# NOTE: auto adding shopping list from meal plan is tested in test_shopping_recipe as tests are identical + + +@pytest.fixture() +def meal_type(space_1, u1_s1): + return MealType.objects.get_or_create(name='test', space=space_1, created_by=auth.get_user(u1_s1))[0] + + +@pytest.fixture() +def obj_1(space_1, recipe_1_s1, meal_type, u1_s1): + return MealPlan.objects.create(recipe=recipe_1_s1, space=space_1, meal_type=meal_type, date=datetime.now(), + created_by=auth.get_user(u1_s1)) + + +@pytest.fixture +def obj_2(space_1, recipe_1_s1, meal_type, u1_s1): + return MealPlan.objects.create(recipe=recipe_1_s1, space=space_1, meal_type=meal_type, date=datetime.now(), + created_by=auth.get_user(u1_s1)) + + +@pytest.mark.parametrize("arg", [ + ['a_u', 403], + ['g1_s1', 200], + ['u1_s1', 200], + ['a1_s1', 200], +]) +def test_list_permission(arg, request): + c = request.getfixturevalue(arg[0]) + assert c.get(reverse(LIST_URL)).status_code == arg[1] + + +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 len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0 + + obj_1.space = space_2 + obj_1.save() + + assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 1 + assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0 + + +def test_list_filter(obj_1, u1_s1): + r = u1_s1.get(reverse(LIST_URL)) + assert r.status_code == 200 + response = json.loads(r.content) + assert len(response) == 1 + + response = json.loads( + u1_s1.get(f'{reverse(LIST_URL)}?from_date={(datetime.now() + timedelta(days=2)).strftime("%Y-%m-%d")}').content) + assert len(response) == 0 + + response = json.loads( + u1_s1.get(f'{reverse(LIST_URL)}?to_date={(datetime.now() - timedelta(days=2)).strftime("%Y-%m-%d")}').content) + assert len(response) == 0 + + response = json.loads(u1_s1.get( + f'{reverse(LIST_URL)}?from_date={(datetime.now() - timedelta(days=2)).strftime("%Y-%m-%d")}&to_date={(datetime.now() + timedelta(days=2)).strftime("%Y-%m-%d")}').content) + assert len(response) == 1 + + +@pytest.mark.parametrize("arg", [ + ['a_u', 403], + ['g1_s1', 404], + ['u1_s1', 200], + ['a1_s1', 404], + ['g1_s2', 404], + ['u1_s2', 404], + ['a1_s2', 404], +]) +def test_update(arg, request, obj_1): + c = request.getfixturevalue(arg[0]) + r = c.patch( + reverse( + DETAIL_URL, + args={obj_1.id} + ), + {'title': 'new'}, + content_type='application/json' + ) + response = json.loads(r.content) + assert r.status_code == arg[1] + if r.status_code == 200: + assert response['title'] == 'new' + + +@pytest.mark.parametrize("arg", [ + ['a_u', 403], + ['g1_s1', 201], + ['u1_s1', 201], + ['a1_s1', 201], +]) +def test_add(arg, request, u1_s2, recipe_1_s1, meal_type): + c = request.getfixturevalue(arg[0]) + r = c.post( + reverse(LIST_URL), + {'recipe': {'id': recipe_1_s1.id, 'name': recipe_1_s1.name, 'keywords': []}, 'meal_type': {'id': meal_type.id, 'name': meal_type.name}, + 'date': (datetime.now()).strftime("%Y-%m-%d"), 'servings': 1, 'title': 'test', 'shared': []}, + content_type='application/json' + ) + response = json.loads(r.content) + print(response) + assert r.status_code == arg[1] + if r.status_code == 201: + assert response['title'] == 'test' + r = c.get(reverse(DETAIL_URL, args={response['id']})) + assert r.status_code == 200 + r = u1_s2.get(reverse(DETAIL_URL, args={response['id']})) + assert r.status_code == 404 + + +def test_delete(u1_s1, u1_s2, obj_1): + r = u1_s2.delete( + reverse( + DETAIL_URL, + args={obj_1.id} + ) + ) + assert r.status_code == 404 + + r = u1_s1.delete( + reverse( + DETAIL_URL, + args={obj_1.id} + ) + ) + + assert r.status_code == 204 + with scopes_disabled(): + assert MealPlan.objects.count() == 0 + + +def test_add_with_shopping(u1_s1, meal_type): + space = meal_type.space + with scope(space=space): + recipe = RecipeFactory.create(space=space) + r = u1_s1.post( + reverse(LIST_URL), + {'recipe': {'id': recipe.id, 'name': recipe.name, 'keywords': []}, 'meal_type': {'id': meal_type.id, 'name': meal_type.name}, + 'date': (datetime.now()).strftime("%Y-%m-%d"), 'servings': 1, 'title': 'test', 'shared': [], 'addshopping': True}, + content_type='application/json' + ) + + assert len(json.loads(u1_s1.get(reverse('api:shoppinglistentry-list')).content)) == 10 diff --git a/cookbook/tests/api/test_api_property_type.py b/cookbook/tests/api/test_api_property_type.py new file mode 100644 index 000000000..d4799789d --- /dev/null +++ b/cookbook/tests/api/test_api_property_type.py @@ -0,0 +1,132 @@ +import json + +import pytest +from django.contrib import auth +from django.urls import reverse +from django_scopes import scopes_disabled + +from cookbook.models import Food, MealType, PropertyType + +LIST_URL = 'api:propertytype-list' +DETAIL_URL = 'api:propertytype-detail' + + +@pytest.fixture() +def obj_1(space_1, u1_s1): + return PropertyType.objects.get_or_create(name='test_1', space=space_1)[0] + + +@pytest.fixture +def obj_2(space_1, u1_s1): + return PropertyType.objects.get_or_create(name='test_2', space=space_1)[0] + + +@pytest.mark.parametrize("arg", [ + ['a_u', 403], + ['g1_s1', 403], + ['u1_s1', 200], + ['a1_s1', 200], +]) +def test_list_permission(arg, request): + c = request.getfixturevalue(arg[0]) + assert c.get(reverse(LIST_URL)).status_code == arg[1] + + +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 len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0 + + obj_1.space = space_2 + obj_1.save() + + assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 1 + assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 1 + + +@pytest.mark.parametrize("arg", [ + ['a_u', 403], + ['g1_s1', 403], + ['u1_s1', 200], + ['a1_s1', 200], + ['g1_s2', 403], + ['u1_s2', 404], + ['a1_s2', 404], +]) +def test_update(arg, request, obj_1): + c = request.getfixturevalue(arg[0]) + r = c.patch( + reverse( + DETAIL_URL, + args={obj_1.id} + ), + {'name': 'new'}, + content_type='application/json' + ) + response = json.loads(r.content) + assert r.status_code == arg[1] + if r.status_code == 200: + assert response['name'] == 'new' + + +@pytest.mark.parametrize("arg", [ + ['a_u', 403], + ['g1_s1', 403], + ['u1_s1', 201], + ['a1_s1', 201], +]) +def test_add(arg, request, u1_s2): + c = request.getfixturevalue(arg[0]) + r = c.post( + reverse(LIST_URL), + {'name': 'test'}, + content_type='application/json' + ) + response = json.loads(r.content) + assert r.status_code == arg[1] + if r.status_code == 201: + assert response['name'] == 'test' + r = c.get(reverse(DETAIL_URL, args={response['id']})) + assert r.status_code == 200 + r = u1_s2.get(reverse(DETAIL_URL, args={response['id']})) + assert r.status_code == 404 + + +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_delete(u1_s1, u1_s2, obj_1): + r = u1_s2.delete( + reverse( + DETAIL_URL, + args={obj_1.id} + ) + ) + assert r.status_code == 404 + + r = u1_s1.delete( + reverse( + DETAIL_URL, + args={obj_1.id} + ) + ) + + assert r.status_code == 204 + with scopes_disabled(): + assert MealType.objects.count() == 0 diff --git a/cookbook/tests/api/test_api_unit_conversion.py b/cookbook/tests/api/test_api_unit_conversion.py new file mode 100644 index 000000000..a45b6e5dd --- /dev/null +++ b/cookbook/tests/api/test_api_unit_conversion.py @@ -0,0 +1,163 @@ +import json + +import pytest +from django.contrib import auth +from django.urls import reverse +from django_scopes import scopes_disabled + +from cookbook.models import Food, MealType, UnitConversion +from cookbook.tests.conftest import get_random_food, get_random_unit + +LIST_URL = 'api:unitconversion-list' +DETAIL_URL = 'api:unitconversion-detail' + + +@pytest.fixture() +def obj_1(space_1, u1_s1): + return UnitConversion.objects.get_or_create( + food=get_random_food(space_1, u1_s1), + base_amount=100, + base_unit=get_random_unit(space_1, u1_s1), + converted_amount=100, + converted_unit=get_random_unit(space_1, u1_s1), + created_by=auth.get_user(u1_s1), + space=space_1 + )[0] + + +@pytest.fixture +def obj_2(space_1, u1_s1): + return UnitConversion.objects.get_or_create( + food=get_random_food(space_1, u1_s1), + base_amount=100, + base_unit=get_random_unit(space_1, u1_s1), + converted_amount=100, + converted_unit=get_random_unit(space_1, u1_s1), + created_by=auth.get_user(u1_s1), + space=space_1 + )[0] + + +@pytest.mark.parametrize("arg", [ + ['a_u', 403], + ['g1_s1', 403], + ['u1_s1', 200], + ['a1_s1', 200], +]) +def test_list_permission(arg, request): + c = request.getfixturevalue(arg[0]) + assert c.get(reverse(LIST_URL)).status_code == arg[1] + + +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 len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0 + + obj_1.space = space_2 + obj_1.save() + + assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 1 + assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 1 + + +@pytest.mark.parametrize("arg", [ + ['a_u', 403], + ['g1_s1', 403], + ['u1_s1', 200], + ['a1_s1', 200], + ['g1_s2', 403], + ['u1_s2', 404], + ['a1_s2', 404], +]) +def test_update(arg, request, obj_1): + c = request.getfixturevalue(arg[0]) + r = c.patch( + reverse( + DETAIL_URL, + args={obj_1.id} + ), + {'base_amount': 1000}, + content_type='application/json' + ) + response = json.loads(r.content) + assert r.status_code == arg[1] + if r.status_code == 200: + assert response['base_amount'] == 1000 + + +@pytest.mark.parametrize("arg", [ + ['a_u', 403], + ['g1_s1', 403], + ['u1_s1', 201], + ['a1_s1', 201], +]) +def test_add(arg, request, u1_s2, space_1, u1_s1): + with scopes_disabled(): + c = request.getfixturevalue(arg[0]) + random_unit_1 = get_random_unit(space_1, u1_s1) + random_unit_2 = get_random_unit(space_1, u1_s1) + random_food_1 = get_random_unit(space_1, u1_s1) + r = c.post( + reverse(LIST_URL), + { + 'food': {'id': random_food_1.id, 'name': random_food_1.name}, + 'base_amount': 100, + 'base_unit': {'id': random_unit_1.id, 'name': random_unit_1.name}, + 'converted_amount': 100, + 'converted_unit': {'id': random_unit_2.id, 'name': random_unit_2.name} + + }, + content_type='application/json' + ) + + response = json.loads(r.content) + print(response) + assert r.status_code == arg[1] + if r.status_code == 201: + assert response['base_amount'] == 100 + r = c.get(reverse(DETAIL_URL, args={response['id']})) + assert r.status_code == 200 + 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 +# assert response['id'] != obj_1.id + + +def test_delete(u1_s1, u1_s2, obj_1): + r = u1_s2.delete( + reverse( + DETAIL_URL, + args={obj_1.id} + ) + ) + assert r.status_code == 404 + + r = u1_s1.delete( + reverse( + DETAIL_URL, + args={obj_1.id} + ) + ) + + assert r.status_code == 204 + with scopes_disabled(): + assert MealType.objects.count() == 0 diff --git a/cookbook/tests/conftest.py b/cookbook/tests/conftest.py index 85f33145d..ecece7cc3 100644 --- a/cookbook/tests/conftest.py +++ b/cookbook/tests/conftest.py @@ -13,6 +13,8 @@ from cookbook.tests.factories import SpaceFactory, UserFactory register(SpaceFactory, 'space_1') register(SpaceFactory, 'space_2') + + # register(FoodFactory, space=LazyFixture('space_2')) # TODO refactor clients to be factories @@ -141,7 +143,7 @@ def validate_recipe(expected, recipe): for k in expected_lists[key]: try: print('comparing ', any([dict_compare(k, i) - for i in target_lists[key]])) + for i in target_lists[key]])) assert any([dict_compare(k, i) for i in target_lists[key]]) except AssertionError: for result in [dict_compare(k, i, details=True) for i in target_lists[key]]: @@ -169,7 +171,6 @@ def dict_compare(d1, d2, details=False): def transpose(text, number=2): - # select random token tokens = text.split() positions = list(i for i, e in enumerate(tokens) if len(e) > 1) @@ -212,6 +213,14 @@ def ext_recipe_1_s1(space_1, u1_s1): return r +def get_random_food(space_1, u1_s1): + return Food.objects.get_or_create(name=str(uuid.uuid4()), space=space_1)[0] + + +def get_random_unit(space_1, u1_s1): + return Unit.objects.get_or_create(name=str(uuid.uuid4()), space=space_1)[0] + + # ---------------------- USER FIXTURES ----------------------- # maybe better with factories but this is very explict so ... diff --git a/cookbook/urls.py b/cookbook/urls.py index 356714d28..976a95e0c 100644 --- a/cookbook/urls.py +++ b/cookbook/urls.py @@ -41,7 +41,7 @@ router.register(r'recipe', api.RecipeViewSet) router.register(r'recipe-book', api.RecipeBookViewSet) router.register(r'recipe-book-entry', api.RecipeBookEntryViewSet) router.register(r'unit-conversion', api.UnitConversionViewSet) -router.register(r'food-property-type', api.FoodPropertyTypeViewSet) +router.register(r'food-property-type', api.PropertyTypeViewSet) router.register(r'food-property', api.FoodPropertyViewSet) router.register(r'shopping-list', api.ShoppingListViewSet) router.register(r'shopping-list-entry', api.ShoppingListEntryViewSet) diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 9681f17c1..d87a8ade4 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -978,7 +978,7 @@ class UnitConversionViewSet(viewsets.ModelViewSet): return self.queryset.filter(space=self.request.space) -class FoodPropertyTypeViewSet(viewsets.ModelViewSet): +class PropertyTypeViewSet(viewsets.ModelViewSet): queryset = PropertyType.objects serializer_class = PropertyTypeSerializer permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]