Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit dbe939a

Browse files
authored
Ensured that URL and id field are kept when using sparse fields (#1231)
Ensured that URL and id field are not filtered out when using sparse fields URL field is considered a field in DRF but is not in JSON:API spec therefore we may not exclude it. ID on the other hand is a required field and may not be filtered.
1 parent 21493c1 commit dbe939a

File tree

6 files changed

+98
-9
lines changed

6 files changed

+98
-9
lines changed

‎CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ any parts of the framework not mentioned in the documentation should generally b
1414

1515
* Added `429 Too Many Requests` as a possible error response in the OpenAPI schema.
1616

17+
### Fixed
18+
19+
* Ensured that URL and id field are kept when using sparse fields (regression since 7.0.0)
20+
1721
## [7.0.0] - 2024年05月02日
1822

1923
### Added

‎rest_framework_json_api/renderers.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,10 @@ def _filter_sparse_fields(cls, serializer, fields, resource_name):
446446
return {
447447
field_name: field
448448
for field_name, field, in fields.items()
449-
if field_name in sparse_fields
449+
if field.field_name in sparse_fields
450+
# URL field is not considered a field in JSON:API spec
451+
# but a link so need to keep it
452+
or field.field_name == api_settings.URL_FIELD_NAME
450453
}
451454

452455
return fields

‎rest_framework_json_api/serializers.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,15 @@ def _readable_fields(self):
9494
field
9595
for field in readable_fields
9696
if field.field_name in sparse_fields
97+
# URL field is not considered a field in JSON:API spec
98+
# but a link so need to keep it
9799
or field.field_name == api_settings.URL_FIELD_NAME
100+
# ID is a required field which might have been overwritten
101+
# so need to keep it
102+
or field.field_name == "id"
98103
)
99104
except AttributeError:
100-
# no type on serializer, must be used only as only nested
105+
# no type on serializer, may only be used nested
101106
pass
102107

103108
return readable_fields

‎tests/serializers.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from rest_framework.settings import api_settings
2+
13
from rest_framework_json_api import serializers
24
from tests.models import (
35
BasicModel,
@@ -32,6 +34,16 @@ class Meta:
3234
)
3335

3436

37+
class ForeignKeySourcetHyperlinkedSerializer(serializers.HyperlinkedModelSerializer):
38+
class Meta:
39+
model = ForeignKeySource
40+
fields = (
41+
"name",
42+
"target",
43+
api_settings.URL_FIELD_NAME,
44+
)
45+
46+
3547
class ManyToManyTargetSerializer(serializers.ModelSerializer):
3648
class Meta:
3749
fields = ("name",)

‎tests/test_views.py

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
from tests.serializers import BasicModelSerializer, ForeignKeyTargetSerializer
1717
from tests.views import (
1818
BasicModelViewSet,
19+
ForeignKeySourcetHyperlinkedViewSet,
1920
ForeignKeySourceViewSet,
21+
ForeignKeyTargetViewSet,
2022
ManyToManySourceViewSet,
2123
NestedRelatedSourceViewSet,
2224
)
@@ -87,7 +89,7 @@ def test_list(self, client, model):
8789

8890
@pytest.mark.urls(__name__)
8991
def test_list_with_include_foreign_key(self, client, foreign_key_source):
90-
url = reverse("foreign-key-source-list")
92+
url = reverse("foreignkeysource-list")
9193
response = client.get(url, data={"include": "target"})
9294
assert response.status_code == status.HTTP_200_OK
9395
result = response.json()
@@ -156,7 +158,7 @@ def test_list_with_include_nested_related_field(
156158

157159
@pytest.mark.urls(__name__)
158160
def test_list_with_invalid_include(self, client, foreign_key_source):
159-
url = reverse("foreign-key-source-list")
161+
url = reverse("foreignkeysource-list")
160162
response = client.get(url, data={"include": "invalid"})
161163
assert response.status_code == status.HTTP_400_BAD_REQUEST
162164
result = response.json()
@@ -195,7 +197,7 @@ def test_retrieve(self, client, model):
195197

196198
@pytest.mark.urls(__name__)
197199
def test_retrieve_with_include_foreign_key(self, client, foreign_key_source):
198-
url = reverse("foreign-key-source-detail", kwargs={"pk": foreign_key_source.pk})
200+
url = reverse("foreignkeysource-detail", kwargs={"pk": foreign_key_source.pk})
199201
response = client.get(url, data={"include": "target"})
200202
assert response.status_code == status.HTTP_200_OK
201203
result = response.json()
@@ -208,6 +210,20 @@ def test_retrieve_with_include_foreign_key(self, client, foreign_key_source):
208210
}
209211
] == result["included"]
210212

213+
@pytest.mark.urls(__name__)
214+
def test_retrieve_hyperlinked_with_sparse_fields(self, client, foreign_key_source):
215+
url = reverse(
216+
"foreignkeysourcehyperlinked-detail", kwargs={"pk": foreign_key_source.pk}
217+
)
218+
response = client.get(url, data={"fields[ForeignKeySource]": "name"})
219+
assert response.status_code == status.HTTP_200_OK
220+
data = response.json()["data"]
221+
assert data["attributes"] == {"name": foreign_key_source.name}
222+
assert "relationships" not in data
223+
assert data["links"] == {
224+
"self": f"http://testserver/foreign_key_sources/{foreign_key_source.pk}/"
225+
}
226+
211227
@pytest.mark.urls(__name__)
212228
def test_patch(self, client, model):
213229
data = {
@@ -239,7 +255,7 @@ def test_delete(self, client, model):
239255

240256
@pytest.mark.urls(__name__)
241257
def test_create_with_sparse_fields(self, client, foreign_key_target):
242-
url = reverse("foreign-key-source-list")
258+
url = reverse("foreignkeysource-list")
243259
data = {
244260
"data": {
245261
"id": None,
@@ -379,6 +395,28 @@ def test_patch_with_custom_id(self, client):
379395
}
380396
}
381397

398+
@pytest.mark.urls(__name__)
399+
def test_patch_with_custom_id_with_sparse_fields(self, client):
400+
data = {
401+
"data": {
402+
"id": 2_193_102,
403+
"type": "custom",
404+
"attributes": {"body": "hello"},
405+
}
406+
}
407+
408+
url = reverse("custom-id")
409+
410+
response = client.patch(f"{url}?fields[custom]=body", data=data)
411+
assert response.status_code == status.HTTP_200_OK
412+
assert response.json() == {
413+
"data": {
414+
"type": "custom",
415+
"id": "2176ce", # get_id() -> hex
416+
"attributes": {"body": "hello"},
417+
}
418+
}
419+
382420

383421
# Routing setup
384422

@@ -415,13 +453,16 @@ class CustomModelSerializer(serializers.Serializer):
415453
id = serializers.IntegerField()
416454

417455

418-
class CustomIdModelSerializer(serializers.Serializer):
456+
class CustomIdSerializer(serializers.Serializer):
419457
id = serializers.SerializerMethodField()
420458
body = serializers.CharField()
421459

422460
def get_id(self, obj):
423461
return hex(obj.id)[2:]
424462

463+
class Meta:
464+
resource_name = "custom"
465+
425466

426467
class CustomAPIView(APIView):
427468
parser_classes = [JSONParser]
@@ -443,14 +484,23 @@ class CustomIdAPIView(APIView):
443484
resource_name = "custom"
444485

445486
def patch(self, request, *args, **kwargs):
446-
serializer = CustomIdModelSerializer(CustomModel(request.data))
487+
serializer = CustomIdSerializer(
488+
CustomModel(request.data), context={"request": self.request}
489+
)
447490
return Response(status=status.HTTP_200_OK, data=serializer.data)
448491

449492

493+
# TODO remove basename and use default (lowercase of model)
494+
# this makes using HyperlinkedIdentityField easier and reduces
495+
# configuration in general
450496
router = SimpleRouter()
451497
router.register(r"basic_models", BasicModelViewSet, basename="basic-model")
498+
router.register(r"foreign_key_sources", ForeignKeySourceViewSet)
499+
router.register(r"foreign_key_targets", ForeignKeyTargetViewSet)
452500
router.register(
453-
r"foreign_key_sources", ForeignKeySourceViewSet, basename="foreign-key-source"
501+
r"foreign_key_sources_hyperlinked",
502+
ForeignKeySourcetHyperlinkedViewSet,
503+
"foreignkeysourcehyperlinked",
454504
)
455505
router.register(
456506
r"many_to_many_sources", ManyToManySourceViewSet, basename="many-to-many-source"

‎tests/views.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22
from tests.models import (
33
BasicModel,
44
ForeignKeySource,
5+
ForeignKeyTarget,
56
ManyToManySource,
67
NestedRelatedSource,
78
)
89
from tests.serializers import (
910
BasicModelSerializer,
1011
ForeignKeySourceSerializer,
12+
ForeignKeySourcetHyperlinkedSerializer,
13+
ForeignKeyTargetSerializer,
1114
ManyToManySourceSerializer,
1215
NestedRelatedSourceSerializer,
1316
)
@@ -25,6 +28,18 @@ class ForeignKeySourceViewSet(ModelViewSet):
2528
ordering = ["name"]
2629

2730

31+
class ForeignKeySourcetHyperlinkedViewSet(ModelViewSet):
32+
serializer_class = ForeignKeySourcetHyperlinkedSerializer
33+
queryset = ForeignKeySource.objects.all()
34+
ordering = ["name"]
35+
36+
37+
class ForeignKeyTargetViewSet(ModelViewSet):
38+
serializer_class = ForeignKeyTargetSerializer
39+
queryset = ForeignKeyTarget.objects.all()
40+
ordering = ["name"]
41+
42+
2843
class ManyToManySourceViewSet(ModelViewSet):
2944
serializer_class = ManyToManySourceSerializer
3045
queryset = ManyToManySource.objects.all()

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /