From 3272b3f68c4d79f80af671caa0f7a16b1c7ecf1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9otim=20HERIN?= Date: Thu, 7 Mar 2024 17:34:31 +0100 Subject: [PATCH 1/9] add specific uuid in create method --- concrete_datastore/api/v1/serializers.py | 21 ++++- tests/tests_api_v1_1/test_api_v1_1_CRUD.py | 97 ++++++++++++++++++++++ 2 files changed, 115 insertions(+), 3 deletions(-) diff --git a/concrete_datastore/api/v1/serializers.py b/concrete_datastore/api/v1/serializers.py index 960bc86c..4b3cb09b 100644 --- a/concrete_datastore/api/v1/serializers.py +++ b/concrete_datastore/api/v1/serializers.py @@ -365,9 +365,11 @@ def make_serializer_class( class Meta: model = concrete.models[meta_model.get_model_name().lower()] fields = _fields - + no_update_fields = [ + "uid", + ] read_only_fields = ( - ['uid', 'created_by', 'admin', 'is_staff'] + ['created_by', 'admin', 'is_staff'] + fk_read_only_fields + [f for f in _all_fields if f.startswith('resource_')] ) @@ -427,7 +429,7 @@ class Meta: attrs.update(custom_fields_attrs) - class _ModelSerializer(serializers.ModelSerializer): + class _ModelSerializer(UpdateMixin, serializers.ModelSerializer): url = serializers.SerializerMethodField() verbose_name = serializers.SerializerMethodField() scopes = serializers.SerializerMethodField() @@ -604,3 +606,16 @@ def __call__(self, value): raise PasswordInsecureValidationError( str(e), code='PASSWORD_INSECURE' ) + + +class UpdateMixin(serializers.ModelSerializer): + def get_extra_kwargs(self): + kwargs = super().get_extra_kwargs() + no_update_fields = getattr(self.Meta, "no_update_fields", None) + + if self.instance and no_update_fields: + for field in no_update_fields: + kwargs.setdefault(field, {}) + kwargs[field]["read_only"] = True + + return kwargs diff --git a/tests/tests_api_v1_1/test_api_v1_1_CRUD.py b/tests/tests_api_v1_1/test_api_v1_1_CRUD.py index a8a95643..f0db64d2 100644 --- a/tests/tests_api_v1_1/test_api_v1_1_CRUD.py +++ b/tests/tests_api_v1_1/test_api_v1_1_CRUD.py @@ -396,3 +396,100 @@ def test_create_with_trailing_space(self): project = Project.objects.first() self.assertEqual(project.name, project_name_to_post) + + def test_create_with_specify_uid(self): + project_name_to_post = "project 1" + url_projects = '/api/v1.1/project/' + + self.assertEqual(Project.objects.count(), 0) + + resp = self.client.post( + url_projects, + { + "uid": "521fd822-12d2-49b1-9573-a1cde74e4d51", + "name": project_name_to_post, + "description": "description", + "skills": [], + "members": [], + }, + HTTP_AUTHORIZATION='Token {}'.format(self.token), + ) + self.assertEqual( + resp.status_code, status.HTTP_201_CREATED, msg=resp.content + ) + + self.assertEqual(Project.objects.count(), 1) + project = Project.objects.first() + + self.assertEqual(project.name, project_name_to_post) + self.assertEqual( + str(project.pk), "521fd822-12d2-49b1-9573-a1cde74e4d51" + ) + + def test_create_with_specify_uid_wrong_format(self): + project_name_to_post = "project 1" + url_projects = '/api/v1.1/project/' + + self.assertEqual(Project.objects.count(), 0) + + resp = self.client.post( + url_projects, + { + "uid": "521fd822-12-a1cde74e4d51", # wrong format + "name": project_name_to_post, + "description": "description", + "skills": [], + "members": [], + }, + HTTP_AUTHORIZATION='Token {}'.format(self.token), + ) + self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(resp.data['uid'][0].title(), 'Must Be A Valid Uuid.') + + def test_error_when_patch_uid_instance(self): + # We can't update uuid of instance + project_name_to_post = "project 1" + url_projects = '/api/v1.1/project/' + + self.assertEqual(Project.objects.count(), 0) + + resp = self.client.post( + url_projects, + { + "uid": "521fd822-12d2-49b1-9573-a1cde74e4d51", + "name": project_name_to_post, + "description": "description", + "skills": [], + "members": [], + }, + HTTP_AUTHORIZATION='Token {}'.format(self.token), + ) + self.assertEqual( + resp.status_code, status.HTTP_201_CREATED, msg=resp.content + ) + + self.assertEqual(Project.objects.count(), 1) + project = Project.objects.first() + + url_to_patch = resp.data['url'] + resp = self.client.patch( + url_to_patch, + {"name": "Project42"}, + HTTP_AUTHORIZATION='Token {}'.format(self.token), + ) + self.assertEqual(resp.status_code, status.HTTP_200_OK) + + new_name = resp.data['name'] + self.assertEqual(new_name, "Project42", msg=resp.content) + + resp = self.client.patch( + url_to_patch, + {"uid": "1af2566f-6dad-4fb4-81f1-2cb4d67a713f"}, + HTTP_AUTHORIZATION='Token {}'.format(self.token), + ) + new_uid = resp.data['uid'] + self.assertEqual(resp.status_code, status.HTTP_200_OK) + + self.assertEqual( + new_uid, "521fd822-12d2-49b1-9573-a1cde74e4d51", msg=resp.content + ) From da53f44e25bcd6caff175ee2acc83f153c64bfff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9otim=20HERIN?= Date: Fri, 8 Mar 2024 11:24:24 +0100 Subject: [PATCH 2/9] rais 400 if user try to update uid with patch request --- concrete_datastore/api/v1/views.py | 11 +++++ tests/tests_api_v1_1/test_api_v1_1_CRUD.py | 49 ++++++++++++++++++++-- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/concrete_datastore/api/v1/views.py b/concrete_datastore/api/v1/views.py index f43b4a78..2b284295 100644 --- a/concrete_datastore/api/v1/views.py +++ b/concrete_datastore/api/v1/views.py @@ -2274,6 +2274,17 @@ def handle_divider_update(self, request, request_user, instance): def update(self, request, *args, **kwargs): instance = self.get_object() + update_uid = request.data.__contains__('uid') + + if request.method == "PATCH" and update_uid is True: + return Response( + data={ + "message": "The field 'uid' can't be updated", + "_errors": ["INVALID_QUERY"], + }, + status=HTTP_400_BAD_REQUEST, + ) + if isinstance(instance, UserModel): request_user = request.user at_least_admin = request_user.is_at_least_admin diff --git a/tests/tests_api_v1_1/test_api_v1_1_CRUD.py b/tests/tests_api_v1_1/test_api_v1_1_CRUD.py index f0db64d2..503b4799 100644 --- a/tests/tests_api_v1_1/test_api_v1_1_CRUD.py +++ b/tests/tests_api_v1_1/test_api_v1_1_CRUD.py @@ -487,9 +487,50 @@ def test_error_when_patch_uid_instance(self): {"uid": "1af2566f-6dad-4fb4-81f1-2cb4d67a713f"}, HTTP_AUTHORIZATION='Token {}'.format(self.token), ) - new_uid = resp.data['uid'] - self.assertEqual(resp.status_code, status.HTTP_200_OK) - + self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(resp.data['_errors'], ['INVALID_QUERY']) self.assertEqual( - new_uid, "521fd822-12d2-49b1-9573-a1cde74e4d51", msg=resp.content + resp.data['message'], "The field 'uid' can't be updated" ) + + def test_update_uid_with_put_request(self): + # We can't update uuid of instance + # The request must not fail but the uid field + # must not be modified + url_projects = '/api/v1.1/project/' + initial_uid = "521fd822-12d2-49b1-9573-a1cde74e4d51" + + project_name_to_post = "project 1" + + self.assertEqual(Project.objects.count(), 0) + + resp = self.client.post( + url_projects, + { + "uid": initial_uid, + "name": project_name_to_post, + "description": "description", + "skills": [], + "members": [], + }, + HTTP_AUTHORIZATION='Token {}'.format(self.token), + ) + + self.assertEqual(Project.objects.count(), 1) + project = User.objects.first() + + url_to_put = resp.data['url'] + new_project_name = "project new" + resp = self.client.put( + url_to_put, + { + "uid": "1af2566f-6dad-4fb4-81f1-2cb4d67a713f", + "name": new_project_name, + }, + HTTP_AUTHORIZATION='Token {}'.format(self.token), + ) + self.assertEqual(resp.status_code, status.HTTP_200_OK) + self.assertEqual(resp.data["name"], new_project_name, msg=resp.content) + self.assertEqual( + resp.data["uid"], initial_uid, msg=resp.content + ) # Must not be modified From 8800e435b2d0e550bf1560fbe5fb0907ae61cacb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9otim=20HERIN?= Date: Fri, 8 Mar 2024 11:25:27 +0100 Subject: [PATCH 3/9] up changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9afd2ea..00b95bcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,8 @@ ### Changed -- nothing changed +- Create instance with spécific uid +- raise 400 if user try to update uid with patch request ### Removed From 21507cd39ae0c38a9fb0621648e5ca76a190f1c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9otim=20HERIN?= Date: Fri, 8 Mar 2024 12:05:38 +0100 Subject: [PATCH 4/9] update: if the uid not changed the request don't failed --- concrete_datastore/api/v1/views.py | 22 ++++----- tests/tests_api_v1_1/test_api_v1_1_CRUD.py | 53 +++++++++++++++++++--- 2 files changed, 58 insertions(+), 17 deletions(-) diff --git a/concrete_datastore/api/v1/views.py b/concrete_datastore/api/v1/views.py index 2b284295..8c5deed6 100644 --- a/concrete_datastore/api/v1/views.py +++ b/concrete_datastore/api/v1/views.py @@ -2274,17 +2274,17 @@ def handle_divider_update(self, request, request_user, instance): def update(self, request, *args, **kwargs): instance = self.get_object() - update_uid = request.data.__contains__('uid') - - if request.method == "PATCH" and update_uid is True: - return Response( - data={ - "message": "The field 'uid' can't be updated", - "_errors": ["INVALID_QUERY"], - }, - status=HTTP_400_BAD_REQUEST, - ) - + request_uid = request.data.get('uid', None) + if request_uid is not None: + initial_uid = str(instance.uid) + if request_uid != initial_uid: + return Response( + data={ + "message": "The field 'uid' can't be updated", + "_errors": ["INVALID_QUERY"], + }, + status=HTTP_400_BAD_REQUEST, + ) if isinstance(instance, UserModel): request_user = request.user at_least_admin = request_user.is_at_least_admin diff --git a/tests/tests_api_v1_1/test_api_v1_1_CRUD.py b/tests/tests_api_v1_1/test_api_v1_1_CRUD.py index 503b4799..2554d0d9 100644 --- a/tests/tests_api_v1_1/test_api_v1_1_CRUD.py +++ b/tests/tests_api_v1_1/test_api_v1_1_CRUD.py @@ -448,6 +448,7 @@ def test_create_with_specify_uid_wrong_format(self): def test_error_when_patch_uid_instance(self): # We can't update uuid of instance + # Test to update only uuid field project_name_to_post = "project 1" url_projects = '/api/v1.1/project/' @@ -493,10 +494,50 @@ def test_error_when_patch_uid_instance(self): resp.data['message'], "The field 'uid' can't be updated" ) + def test_request_patch_with_same_uid(self): + # Test a patch request with the same uuid, + # the request must not fail but the uid must not change + project_name_to_post = "project 1" + url_projects = '/api/v1.1/project/' + initial_uid = "521fd822-12d2-49b1-9573-a1cde74e4d51" + + self.assertEqual(Project.objects.count(), 0) + + resp = self.client.post( + url_projects, + { + "uid": initial_uid, + "name": project_name_to_post, + "description": "description", + "skills": [], + "members": [], + }, + HTTP_AUTHORIZATION='Token {}'.format(self.token), + ) + self.assertEqual( + resp.status_code, status.HTTP_201_CREATED, msg=resp.content + ) + + self.assertEqual(Project.objects.count(), 1) + project = Project.objects.first() + + url_to_patch = resp.data['url'] + new_project_name = "project new" + + resp = self.client.patch( + url_to_patch, + { + "uid": initial_uid, + "name": new_project_name, + }, + HTTP_AUTHORIZATION='Token {}'.format(self.token), + ) + + self.assertEqual(resp.status_code, status.HTTP_200_OK) + self.assertEqual(resp.data["uid"], initial_uid, msg=resp.content) + self.assertEqual(resp.data["name"], new_project_name, msg=resp.content) + def test_update_uid_with_put_request(self): - # We can't update uuid of instance - # The request must not fail but the uid field - # must not be modified url_projects = '/api/v1.1/project/' initial_uid = "521fd822-12d2-49b1-9573-a1cde74e4d51" @@ -529,8 +570,8 @@ def test_update_uid_with_put_request(self): }, HTTP_AUTHORIZATION='Token {}'.format(self.token), ) - self.assertEqual(resp.status_code, status.HTTP_200_OK) - self.assertEqual(resp.data["name"], new_project_name, msg=resp.content) + self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(resp.data['_errors'], ['INVALID_QUERY']) self.assertEqual( - resp.data["uid"], initial_uid, msg=resp.content + resp.data['message'], "The field 'uid' can't be updated" ) # Must not be modified From 4c07198c4c2caae39b2082b9da05dc46f646564d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9otim=20HERIN?= Date: Fri, 8 Mar 2024 12:07:14 +0100 Subject: [PATCH 5/9] update: change name of variable --- concrete_datastore/api/v1/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/concrete_datastore/api/v1/views.py b/concrete_datastore/api/v1/views.py index 8c5deed6..31fc22a6 100644 --- a/concrete_datastore/api/v1/views.py +++ b/concrete_datastore/api/v1/views.py @@ -2276,8 +2276,8 @@ def update(self, request, *args, **kwargs): instance = self.get_object() request_uid = request.data.get('uid', None) if request_uid is not None: - initial_uid = str(instance.uid) - if request_uid != initial_uid: + instance_uid = str(instance.uid) + if request_uid != instance_uid: return Response( data={ "message": "The field 'uid' can't be updated", From 55f72bf0029918e13c973678d1894e6a089a7f49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9otim=20HERIN?= Date: Mon, 11 Mar 2024 10:20:24 +0100 Subject: [PATCH 6/9] refactoring to update accept uid in post request --- concrete_datastore/api/v1/views.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/concrete_datastore/api/v1/views.py b/concrete_datastore/api/v1/views.py index 31fc22a6..19947ce9 100644 --- a/concrete_datastore/api/v1/views.py +++ b/concrete_datastore/api/v1/views.py @@ -2274,10 +2274,10 @@ def handle_divider_update(self, request, request_user, instance): def update(self, request, *args, **kwargs): instance = self.get_object() - request_uid = request.data.get('uid', None) - if request_uid is not None: - instance_uid = str(instance.uid) - if request_uid != instance_uid: + request_uid = str(request.data.get('uid', '')) + if request_uid: + initial_uid = str(instance.uid) + if request_uid != initial_uid: return Response( data={ "message": "The field 'uid' can't be updated", From 1b4d9778e9ffc566ea48ebd9f0e39eed9aa226cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9otim=20HERIN?= Date: Mon, 11 Mar 2024 10:23:37 +0100 Subject: [PATCH 7/9] change variable initial_uid to instance_uid --- concrete_datastore/api/v1/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/concrete_datastore/api/v1/views.py b/concrete_datastore/api/v1/views.py index 19947ce9..e27372f4 100644 --- a/concrete_datastore/api/v1/views.py +++ b/concrete_datastore/api/v1/views.py @@ -2276,8 +2276,8 @@ def update(self, request, *args, **kwargs): instance = self.get_object() request_uid = str(request.data.get('uid', '')) if request_uid: - initial_uid = str(instance.uid) - if request_uid != initial_uid: + instance_uid = str(instance.uid) + if request_uid != instance_uid: return Response( data={ "message": "The field 'uid' can't be updated", From a257d0f63f20720fe2005bafca4408da636dcbbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9otim=20HERIN?= Date: Mon, 11 Mar 2024 10:41:26 +0100 Subject: [PATCH 8/9] cahnge double quote message txt --- concrete_datastore/api/v1/views.py | 4 ++-- tests/tests_api_v1_1/test_api_v1_1_CRUD.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/concrete_datastore/api/v1/views.py b/concrete_datastore/api/v1/views.py index e27372f4..a2124d0c 100644 --- a/concrete_datastore/api/v1/views.py +++ b/concrete_datastore/api/v1/views.py @@ -2280,8 +2280,8 @@ def update(self, request, *args, **kwargs): if request_uid != instance_uid: return Response( data={ - "message": "The field 'uid' can't be updated", - "_errors": ["INVALID_QUERY"], + 'message': 'The field "uid" can\'t be updated', + '_errors': ['INVALID_QUERY'], }, status=HTTP_400_BAD_REQUEST, ) diff --git a/tests/tests_api_v1_1/test_api_v1_1_CRUD.py b/tests/tests_api_v1_1/test_api_v1_1_CRUD.py index 2554d0d9..58bac352 100644 --- a/tests/tests_api_v1_1/test_api_v1_1_CRUD.py +++ b/tests/tests_api_v1_1/test_api_v1_1_CRUD.py @@ -491,7 +491,7 @@ def test_error_when_patch_uid_instance(self): self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(resp.data['_errors'], ['INVALID_QUERY']) self.assertEqual( - resp.data['message'], "The field 'uid' can't be updated" + resp.data['message'], 'The field "uid" can\'t be updated' ) def test_request_patch_with_same_uid(self): @@ -573,5 +573,5 @@ def test_update_uid_with_put_request(self): self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(resp.data['_errors'], ['INVALID_QUERY']) self.assertEqual( - resp.data['message'], "The field 'uid' can't be updated" + resp.data['message'], 'The field "uid" can\'t be updated' ) # Must not be modified From 3afd4bdcb4550c648877218d82c0a4c945c1fd52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9otim=20HERIN?= Date: Mon, 11 Mar 2024 11:46:05 +0100 Subject: [PATCH 9/9] delete update_mixin --- concrete_datastore/api/v1/serializers.py | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/concrete_datastore/api/v1/serializers.py b/concrete_datastore/api/v1/serializers.py index 4b3cb09b..b50aef3f 100644 --- a/concrete_datastore/api/v1/serializers.py +++ b/concrete_datastore/api/v1/serializers.py @@ -365,9 +365,7 @@ def make_serializer_class( class Meta: model = concrete.models[meta_model.get_model_name().lower()] fields = _fields - no_update_fields = [ - "uid", - ] + read_only_fields = ( ['created_by', 'admin', 'is_staff'] + fk_read_only_fields @@ -429,7 +427,7 @@ class Meta: attrs.update(custom_fields_attrs) - class _ModelSerializer(UpdateMixin, serializers.ModelSerializer): + class _ModelSerializer(serializers.ModelSerializer): url = serializers.SerializerMethodField() verbose_name = serializers.SerializerMethodField() scopes = serializers.SerializerMethodField() @@ -606,16 +604,3 @@ def __call__(self, value): raise PasswordInsecureValidationError( str(e), code='PASSWORD_INSECURE' ) - - -class UpdateMixin(serializers.ModelSerializer): - def get_extra_kwargs(self): - kwargs = super().get_extra_kwargs() - no_update_fields = getattr(self.Meta, "no_update_fields", None) - - if self.instance and no_update_fields: - for field in no_update_fields: - kwargs.setdefault(field, {}) - kwargs[field]["read_only"] = True - - return kwargs