Skip to content

Commit 53469f3

Browse files
authored
Re-enabled overwriting of URL field (#1237)
Enabled overwriting of URL field URL_FIELD_NAME is usually used as self-link in links. However it should be allowed to be overwritten as long as not HyperlinkedIdentifyField has been used.
1 parent 3f1ea67 commit 53469f3

File tree

8 files changed

+89
-8
lines changed

8 files changed

+89
-8
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
Note that in line with [Django REST framework policy](https://www.django-rest-framework.org/topics/release-notes/),
99
any parts of the framework not mentioned in the documentation should generally be considered private API, and may be subject to change.
1010

11+
## [Unreleased]
12+
13+
### Fixed
14+
15+
* Re-enabled overwriting of url field (regression since 7.0.0)
16+
1117
## [7.0.1] - 2024-06-06
1218

1319
### Added

rest_framework_json_api/renderers.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,11 @@ def extract_attributes(cls, fields, resource):
7575
and relationships are not returned.
7676
"""
7777

78-
invalid_fields = {"id", api_settings.URL_FIELD_NAME}
79-
8078
return {
8179
format_field_name(field_name): value
8280
for field_name, value in resource.items()
8381
if field_name in fields
84-
and field_name not in invalid_fields
82+
and field_name != "id"
8583
and not is_relationship_field(fields[field_name])
8684
}
8785

@@ -449,7 +447,10 @@ def _filter_sparse_fields(cls, serializer, fields, resource_name):
449447
if field.field_name in sparse_fields
450448
# URL field is not considered a field in JSON:API spec
451449
# but a link so need to keep it
452-
or field.field_name == api_settings.URL_FIELD_NAME
450+
or (
451+
field.field_name == api_settings.URL_FIELD_NAME
452+
and isinstance(field, relations.HyperlinkedIdentityField)
453+
)
453454
}
454455

455456
return fields
@@ -486,7 +487,7 @@ def build_json_resource_obj(
486487
resource_data["relationships"] = relationships
487488
# Add 'self' link if field is present and valid
488489
if api_settings.URL_FIELD_NAME in resource and isinstance(
489-
fields[api_settings.URL_FIELD_NAME], relations.RelatedField
490+
fields[api_settings.URL_FIELD_NAME], relations.HyperlinkedIdentityField
490491
):
491492
resource_data["links"] = {"self": resource[api_settings.URL_FIELD_NAME]}
492493

rest_framework_json_api/serializers.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from django.utils.module_loading import import_string as import_class_from_dotted_path
77
from django.utils.translation import gettext_lazy as _
88
from rest_framework.exceptions import ParseError
9+
from rest_framework.relations import HyperlinkedIdentityField
910

1011
# star import defined so `rest_framework_json_api.serializers` can be
1112
# a simple drop in for `rest_framework.serializers`
@@ -94,9 +95,12 @@ def _readable_fields(self):
9495
field
9596
for field in readable_fields
9697
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
99-
or field.field_name == api_settings.URL_FIELD_NAME
98+
# URL_FIELD_NAME is the field used as self-link to resource
99+
# however only when it is a HyperlinkedIdentityField
100+
or (
101+
field.field_name == api_settings.URL_FIELD_NAME
102+
and isinstance(field, HyperlinkedIdentityField)
103+
)
100104
# ID is a required field which might have been overwritten
101105
# so need to keep it
102106
or field.field_name == "id"

tests/conftest.py

+6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
ManyToManySource,
99
ManyToManyTarget,
1010
NestedRelatedSource,
11+
URLModel,
1112
)
1213

1314

@@ -36,6 +37,11 @@ def model(db):
3637
return BasicModel.objects.create(text="Model")
3738

3839

40+
@pytest.fixture
41+
def url_instance(db):
42+
return URLModel.objects.create(text="Url", url="https://example.com")
43+
44+
3945
@pytest.fixture
4046
def foreign_key_target(db):
4147
return ForeignKeyTarget.objects.create(name="Target")

tests/models.py

+8
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ class Meta:
1818
ordering = ("id",)
1919

2020

21+
class URLModel(DJAModel):
22+
url = models.URLField()
23+
text = models.CharField(max_length=100)
24+
25+
class Meta:
26+
ordering = ("id",)
27+
28+
2129
# Models for relations tests
2230
# ManyToMany
2331
class ManyToManyTarget(DJAModel):

tests/serializers.py

+10
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
ManyToManySource,
99
ManyToManyTarget,
1010
NestedRelatedSource,
11+
URLModel,
1112
)
1213

1314

@@ -17,6 +18,15 @@ class Meta:
1718
model = BasicModel
1819

1920

21+
class URLModelSerializer(serializers.ModelSerializer):
22+
class Meta:
23+
fields = (
24+
"text",
25+
"url",
26+
)
27+
model = URLModel
28+
29+
2030
class ForeignKeyTargetSerializer(serializers.ModelSerializer):
2131
class Meta:
2232
fields = ("name",)

tests/test_views.py

+38
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
ForeignKeyTargetViewSet,
2222
ManyToManySourceViewSet,
2323
NestedRelatedSourceViewSet,
24+
URLModelViewSet,
2425
)
2526

2627

@@ -182,6 +183,42 @@ def test_list_with_default_included_resources(self, client, foreign_key_source):
182183
}
183184
] == result["included"]
184185

186+
@pytest.mark.urls(__name__)
187+
def test_list_allow_overwriting_url_field(self, client, url_instance):
188+
"""
189+
Test overwriting of url is possible.
190+
191+
URL_FIELD_NAME which is set to 'url' per default is used as self in links.
192+
However if field is overwritten and not a HyperlinkedIdentityField it should be allowed
193+
to use as a attribute as well.
194+
"""
195+
196+
url = reverse("urlmodel-list")
197+
response = client.get(url)
198+
assert response.status_code == status.HTTP_200_OK
199+
data = response.json()["data"]
200+
assert data == [
201+
{
202+
"type": "URLModel",
203+
"id": str(url_instance.pk),
204+
"attributes": {"text": "Url", "url": "https://example.com"},
205+
}
206+
]
207+
208+
@pytest.mark.urls(__name__)
209+
def test_list_allow_overwiritng_url_with_sparse_fields(self, client, url_instance):
210+
url = reverse("urlmodel-list")
211+
response = client.get(url, data={"fields[URLModel]": "text"})
212+
assert response.status_code == status.HTTP_200_OK
213+
data = response.json()["data"]
214+
assert data == [
215+
{
216+
"type": "URLModel",
217+
"id": str(url_instance.pk),
218+
"attributes": {"text": "Url"},
219+
}
220+
]
221+
185222
@pytest.mark.urls(__name__)
186223
def test_retrieve(self, client, model):
187224
url = reverse("basic-model-detail", kwargs={"pk": model.pk})
@@ -495,6 +532,7 @@ def patch(self, request, *args, **kwargs):
495532
# configuration in general
496533
router = SimpleRouter()
497534
router.register(r"basic_models", BasicModelViewSet, basename="basic-model")
535+
router.register(r"url_models", URLModelViewSet)
498536
router.register(r"foreign_key_sources", ForeignKeySourceViewSet)
499537
router.register(r"foreign_key_targets", ForeignKeyTargetViewSet)
500538
router.register(

tests/views.py

+8
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
ForeignKeyTarget,
66
ManyToManySource,
77
NestedRelatedSource,
8+
URLModel,
89
)
910
from tests.serializers import (
1011
BasicModelSerializer,
@@ -13,6 +14,7 @@
1314
ForeignKeyTargetSerializer,
1415
ManyToManySourceSerializer,
1516
NestedRelatedSourceSerializer,
17+
URLModelSerializer,
1618
)
1719

1820

@@ -22,6 +24,12 @@ class BasicModelViewSet(ModelViewSet):
2224
ordering = ["text"]
2325

2426

27+
class URLModelViewSet(ModelViewSet):
28+
serializer_class = URLModelSerializer
29+
queryset = URLModel.objects.all()
30+
ordering = ["url"]
31+
32+
2533
class ForeignKeySourceViewSet(ModelViewSet):
2634
serializer_class = ForeignKeySourceSerializer
2735
queryset = ForeignKeySource.objects.all()

0 commit comments

Comments
 (0)