From e83565b1f2cf0390bfd433b0a82fd67a2c2ac164 Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Wed, 8 Jul 2020 23:19:39 +0200 Subject: [PATCH] added api endpoints and tests --- .../migrations/0073_auto_20200708_2311.py | 18 ++++++ cookbook/models.py | 2 +- cookbook/serializer.py | 14 ++++- cookbook/templates/meal_plan.html | 6 +- cookbook/tests/api/test_api_storage.py | 61 +++++++++++++++++++ cookbook/tests/api/test_api_username.py | 27 ++++++++ cookbook/urls.py | 5 +- cookbook/views/api.py | 17 ++++-- 8 files changed, 136 insertions(+), 14 deletions(-) create mode 100644 cookbook/migrations/0073_auto_20200708_2311.py create mode 100644 cookbook/tests/api/test_api_storage.py create mode 100644 cookbook/tests/api/test_api_username.py diff --git a/cookbook/migrations/0073_auto_20200708_2311.py b/cookbook/migrations/0073_auto_20200708_2311.py new file mode 100644 index 000000000..3b9667a96 --- /dev/null +++ b/cookbook/migrations/0073_auto_20200708_2311.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.7 on 2020-07-08 21:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cookbook', '0072_step_show_as_header'), + ] + + operations = [ + migrations.AlterField( + model_name='sync', + name='last_checked', + field=models.DateTimeField(null=True), + ), + ] diff --git a/cookbook/models.py b/cookbook/models.py index 6b8ed5c0c..2cd4cdbff 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -93,7 +93,7 @@ class Sync(models.Model): storage = models.ForeignKey(Storage, on_delete=models.PROTECT) path = models.CharField(max_length=512, default="") active = models.BooleanField(default=True) - last_checked = models.DateTimeField() + last_checked = models.DateTimeField(null=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) diff --git a/cookbook/serializer.py b/cookbook/serializer.py index 037bc007d..4abb415ad 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -27,9 +27,14 @@ class CustomDecimalField(serializers.Field): class UserNameSerializer(serializers.ModelSerializer): + username = serializers.SerializerMethodField('get_user_label') + + def get_user_label(self, obj): + return obj.get_user_name() + class Meta: model = User - fields = ('id', 'username', 'first_name', 'last_name') + fields = ('id', 'username') class UserPreferenceSerializer(serializers.ModelSerializer): @@ -42,7 +47,12 @@ class UserPreferenceSerializer(serializers.ModelSerializer): class StorageSerializer(serializers.ModelSerializer): class Meta: model = Storage - fields = ('id', 'name', 'method', 'username', 'created_by') + fields = ('id', 'name', 'method', 'username', 'password', 'token', 'created_by') + + extra_kwargs = { + 'password': {'write_only': True}, + 'token': {'write_only': True}, + } class SyncSerializer(serializers.ModelSerializer): diff --git a/cookbook/templates/meal_plan.html b/cookbook/templates/meal_plan.html index 792716e95..b84e444c7 100644 --- a/cookbook/templates/meal_plan.html +++ b/cookbook/templates/meal_plan.html @@ -470,11 +470,7 @@ updateUserNames: function () { return this.$http.get("{% url 'api:username-list' %}?filter_list=[" + this.user_id_update + ']').then((response) => { for (let u of response.data) { - let name = u.username - if (`${u.first_name} ${u.last_name}` !== ' ') { - name = `${u.first_name} ${u.last_name}` - } - this.$set(this.user_names, u.id, name); + this.$set(this.user_names, u.id, u.username); } }).catch((err) => { diff --git a/cookbook/tests/api/test_api_storage.py b/cookbook/tests/api/test_api_storage.py new file mode 100644 index 000000000..67300f48a --- /dev/null +++ b/cookbook/tests/api/test_api_storage.py @@ -0,0 +1,61 @@ +import json + +from django.contrib import auth +from django.db.models import ProtectedError +from django.urls import reverse + +from cookbook.models import Storage, Sync +from cookbook.tests.views.test_views import TestViews + + +class TestApiStorage(TestViews): + + def setUp(self): + super(TestApiStorage, self).setUp() + self.storage = Storage.objects.create( + name='Test Storage', + username='test', + password='password', + token='token', + url='url', + created_by=auth.get_user(self.admin_client_1) + ) + + def test_storage_list(self): + # verify view permissions are applied accordingly + self.batch_requests([(self.anonymous_client, 403), (self.guest_client_1, 403), (self.user_client_1, 403), (self.admin_client_1, 200), (self.superuser_client, 200)], + reverse('api:storage-list')) + + # verify storage is returned + r = self.admin_client_1.get(reverse('api:storage-list')) + self.assertEqual(r.status_code, 200) + response = json.loads(r.content) + self.assertEqual(len(response), 1) + storage_response = response[0] + self.assertEqual(storage_response['name'], self.storage.name) + self.assertFalse('password' in storage_response) + self.assertFalse('token' in storage_response) + + def test_storage_update(self): + # can update storage as admin + r = self.admin_client_1.patch(reverse('api:storage-detail', args={self.storage.id}), {'name': 'new', 'password': 'new_password'}, content_type='application/json') + response = json.loads(r.content) + self.assertEqual(r.status_code, 200) + self.assertEqual(response['name'], 'new') + + # verify password was updated (write only field) + self.storage.refresh_from_db() + self.assertEqual(self.storage.password, 'new_password') + + def test_storage_delete(self): + # can delete storage as admin + r = self.admin_client_1.delete(reverse('api:storage-detail', args={self.storage.id})) + self.assertEqual(r.status_code, 204) + self.assertEqual(Storage.objects.count(), 0) + + self.storage = Storage.objects.create(created_by=auth.get_user(self.admin_client_1), name='test protect') + Sync.objects.create(storage=self.storage, ) + + # test if deleting a storage with existing sync fails (as sync protects storage) + with self.assertRaises(ProtectedError): + self.admin_client_1.delete(reverse('api:storage-detail', args={self.storage.id})) diff --git a/cookbook/tests/api/test_api_username.py b/cookbook/tests/api/test_api_username.py new file mode 100644 index 000000000..23f3c2732 --- /dev/null +++ b/cookbook/tests/api/test_api_username.py @@ -0,0 +1,27 @@ +import json + +from django.contrib import auth +from django.urls import reverse + +from cookbook.models import UserPreference +from cookbook.tests.views.test_views import TestViews + + +class TestApiUsername(TestViews): + + def setUp(self): + super(TestApiUsername, self).setUp() + + def test_forbidden_methods(self): + r = self.user_client_1.post(reverse('api:username-list')) + self.assertEqual(r.status_code, 405) + + r = self.user_client_1.put(reverse('api:username-detail', args=[auth.get_user(self.user_client_1).pk])) + self.assertEqual(r.status_code, 405) + + r = self.user_client_1.delete(reverse('api:username-detail', args=[auth.get_user(self.user_client_1).pk])) + self.assertEqual(r.status_code, 405) + + def test_username_list(self): + self.batch_requests([(self.anonymous_client, 403), (self.guest_client_1, 200), (self.user_client_1, 200), (self.admin_client_1, 200), (self.superuser_client, 200)], + reverse('api:username-list')) diff --git a/cookbook/urls.py b/cookbook/urls.py index 11e2613b8..c20fcb7b3 100644 --- a/cookbook/urls.py +++ b/cookbook/urls.py @@ -9,7 +9,10 @@ from cookbook.views import api, import_export from cookbook.helper import dal router = routers.DefaultRouter() +router.register(r'user-name', api.UserNameViewSet, basename='username') router.register(r'user-preference', api.UserPreferenceViewSet) +router.register(r'storage', api.StorageViewSet) + router.register(r'unit', api.UnitViewSet) router.register(r'food', api.FoodViewSet) router.register(r'step', api.StepViewSet) @@ -19,7 +22,7 @@ router.register(r'ingredient', api.IngredientViewSet) router.register(r'meal-plan', api.MealPlanViewSet) router.register(r'meal-type', api.MealTypeViewSet) router.register(r'view-log', api.ViewLogViewSet) -router.register(r'user-name', api.UserNameViewSet, basename='username') + urlpatterns = [ path('', views.index, name='index'), diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 2091f34b5..f7062abce 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -21,16 +21,16 @@ from rest_framework.mixins import RetrieveModelMixin, UpdateModelMixin, ListMode from rest_framework.parsers import JSONParser, FileUploadParser, MultiPartParser from rest_framework.response import Response -from cookbook.helper.permission_helper import group_required, CustomIsOwner, CustomIsAdmin, CustomIsUser +from cookbook.helper.permission_helper import group_required, CustomIsOwner, CustomIsAdmin, CustomIsUser, CustomIsGuest from cookbook.helper.recipe_url_import import get_from_html from cookbook.models import Recipe, Sync, Storage, CookLog, MealPlan, MealType, ViewLog, UserPreference, RecipeBook, Ingredient, Food, Step, Keyword, Unit from cookbook.provider.dropbox import Dropbox from cookbook.provider.nextcloud import Nextcloud from cookbook.serializer import MealPlanSerializer, MealTypeSerializer, RecipeSerializer, ViewLogSerializer, UserNameSerializer, UserPreferenceSerializer, RecipeBookSerializer, IngredientSerializer, FoodSerializer, StepSerializer, \ - KeywordSerializer, RecipeImageSerializer + KeywordSerializer, RecipeImageSerializer, StorageSerializer -class UserNameViewSet(viewsets.ModelViewSet): +class UserNameViewSet(viewsets.ReadOnlyModelViewSet): """ list: optional parameters @@ -39,11 +39,11 @@ class UserNameViewSet(viewsets.ModelViewSet): """ queryset = User.objects.all() serializer_class = UserNameSerializer - permission_classes = [permissions.IsAuthenticated] + permission_classes = [CustomIsGuest] http_method_names = ['get'] def get_queryset(self): - queryset = User.objects.all() + queryset = self.queryset try: filter_list = self.request.query_params.get('filter_list', None) if filter_list is not None: @@ -70,6 +70,13 @@ class UserPreferenceViewSet(viewsets.ModelViewSet): return self.queryset.filter(user=self.request.user) +class StorageViewSet(viewsets.ModelViewSet): + # TODO handle delete protect error and adjust test + queryset = Storage.objects.all() + serializer_class = StorageSerializer + permission_classes = [CustomIsAdmin, ] + + class RecipeBookViewSet(RetrieveModelMixin, UpdateModelMixin, ListModelMixin, viewsets.GenericViewSet): queryset = RecipeBook.objects.all() serializer_class = RecipeBookSerializer