Skip to content

Commit c503748

Browse files
Support formatting URL segments via new FORMAT_LINKS setting (#876)
Fixes #790.
1 parent 3833271 commit c503748

File tree

10 files changed

+200
-8
lines changed

10 files changed

+200
-8
lines changed

Diff for: AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Jason Housley <[email protected]>
1515
Jerel Unruh <[email protected]>
1616
Jonathan Senecal <[email protected]>
1717
Joseba Mendivil <[email protected]>
18+
Kevin Partington <[email protected]>
1819
Kieran Evans <[email protected]>
1920
2021

Diff for: CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ any parts of the framework not mentioned in the documentation should generally b
1313
### Added
1414

1515
* Ability for the user to select `included_serializers` to apply when using `BrowsableAPI`, based on available `included_serializers` defined for the current endpoint.
16+
* Ability for the user to format serializer properties in URL segments using the `JSON_API_FORMAT_LINKS` setting.
1617

1718
### Fixed
1819

Diff for: docs/usage.md

+38
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,44 @@ When set to pluralize:
477477
}
478478
```
479479

480+
#### Related URL segments
481+
482+
Serializer properties in relationship and related resource URLs may be infected using the `JSON_API_FORMAT_LINKS` setting.
483+
484+
``` python
485+
JSON_API_FORMAT_LINKS = 'dasherize'
486+
```
487+
488+
For example, with a serializer property `created_by` and with `'dasherize'` formatting:
489+
490+
```json
491+
{
492+
"data": {
493+
"type": "comments",
494+
"id": "1",
495+
"attributes": {
496+
"text": "Comments are fun!"
497+
},
498+
"links": {
499+
"self": "/comments/1"
500+
},
501+
"relationships": {
502+
"created_by": {
503+
"links": {
504+
"self": "/comments/1/relationships/created-by",
505+
"related": "/comments/1/created-by"
506+
}
507+
}
508+
}
509+
},
510+
"links": {
511+
"self": "/comments/1"
512+
}
513+
}
514+
```
515+
516+
The relationship name is formatted by the `JSON_API_FORMAT_FIELD_NAMES` setting, but the URL segments are formatted by the `JSON_API_FORMAT_LINKS` setting.
517+
480518
### Related fields
481519

482520
#### ResourceRelatedField

Diff for: rest_framework_json_api/relations.py

+4-7
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from rest_framework_json_api.exceptions import Conflict
1616
from rest_framework_json_api.utils import (
1717
Hyperlink,
18+
format_link_segment,
1819
get_included_serializers,
1920
get_resource_type_from_instance,
2021
get_resource_type_from_queryset,
@@ -112,14 +113,10 @@ def get_links(self, obj=None, lookup_field="pk"):
112113
else view.kwargs[lookup_field]
113114
}
114115

116+
field_name = self.field_name if self.field_name else self.parent.field_name
117+
115118
self_kwargs = kwargs.copy()
116-
self_kwargs.update(
117-
{
118-
"related_field": self.field_name
119-
if self.field_name
120-
else self.parent.field_name
121-
}
122-
)
119+
self_kwargs.update({"related_field": format_link_segment(field_name)})
123120
self_link = self.get_url("self", self.self_link_view_name, self_kwargs, request)
124121

125122
# Assuming RelatedField will be declared in two ways:

Diff for: rest_framework_json_api/settings.py

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
DEFAULTS = {
1313
"FORMAT_FIELD_NAMES": False,
1414
"FORMAT_TYPES": False,
15+
"FORMAT_LINKS": False,
1516
"PLURALIZE_TYPES": False,
1617
"UNIFORM_EXCEPTIONS": False,
1718
}

Diff for: rest_framework_json_api/utils.py

+13
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,19 @@ def format_resource_type(value, format_type=None, pluralize=None):
148148
return inflection.pluralize(value) if pluralize else value
149149

150150

151+
def format_link_segment(value, format_type=None):
152+
"""
153+
Takes a string value and returns it with formatted keys as set in `format_type`
154+
or `JSON_API_FORMAT_LINKS`.
155+
156+
:format_type: Either 'dasherize', 'camelize', 'capitalize' or 'underscore'
157+
"""
158+
if format_type is None:
159+
format_type = json_api_settings.FORMAT_LINKS
160+
161+
return format_value(value, format_type)
162+
163+
151164
def get_related_resource_type(relation):
152165
from rest_framework_json_api.serializers import PolymorphicModelSerializer
153166

Diff for: rest_framework_json_api/views.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from rest_framework_json_api.utils import (
2626
Hyperlink,
2727
OrderedDict,
28+
format_value,
2829
get_included_resources,
2930
get_resource_type_from_instance,
3031
)
@@ -185,7 +186,8 @@ def get_related_serializer_class(self):
185186
return parent_serializer_class
186187

187188
def get_related_field_name(self):
188-
return self.kwargs["related_field"]
189+
field_name = self.kwargs["related_field"]
190+
return format_value(field_name, "underscore")
189191

190192
def get_related_instance(self):
191193
parent_obj = self.get_object()

Diff for: tests/test_relations.py

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import pytest
2+
from django.conf.urls import re_path
3+
from rest_framework.routers import SimpleRouter
4+
5+
from rest_framework_json_api.relations import HyperlinkedRelatedField
6+
from rest_framework_json_api.views import ModelViewSet, RelationshipView
7+
8+
from .models import BasicModel
9+
10+
11+
@pytest.mark.urls(__name__)
12+
@pytest.mark.parametrize(
13+
"format_links,expected_url_segment",
14+
[
15+
(None, "relatedField_name"),
16+
("dasherize", "related-field-name"),
17+
("camelize", "relatedFieldName"),
18+
("capitalize", "RelatedFieldName"),
19+
("underscore", "related_field_name"),
20+
],
21+
)
22+
def test_relationship_urls_respect_format_links(
23+
settings, format_links, expected_url_segment
24+
):
25+
settings.JSON_API_FORMAT_LINKS = format_links
26+
27+
model = BasicModel(text="Some text")
28+
29+
field = HyperlinkedRelatedField(
30+
self_link_view_name="basic-model-relationships",
31+
related_link_view_name="basic-model-related",
32+
read_only=True,
33+
)
34+
field.field_name = "relatedField_name"
35+
36+
expected = {
37+
"self": f"/basic_models/{model.pk}/relationships/{expected_url_segment}/",
38+
"related": f"/basic_models/{model.pk}/{expected_url_segment}/",
39+
}
40+
41+
actual = field.get_links(model)
42+
43+
assert expected == actual
44+
45+
46+
# Routing setup
47+
48+
49+
class BasicModelViewSet(ModelViewSet):
50+
class Meta:
51+
model = BasicModel
52+
53+
54+
class BasicModelRelationshipView(RelationshipView):
55+
queryset = BasicModel.objects
56+
57+
58+
router = SimpleRouter()
59+
router.register(r"basic_models", BasicModelViewSet, basename="basic-model")
60+
61+
urlpatterns = [
62+
re_path(
63+
r"^basic_models/(?P<pk>[^/.]+)/(?P<related_field>[^/.]+)/$",
64+
BasicModelViewSet.as_view({"get": "retrieve_related"}),
65+
name="basic-model-related",
66+
),
67+
re_path(
68+
r"^basic_models/(?P<pk>[^/.]+)/relationships/(?P<related_field>[^/.]+)/$",
69+
BasicModelRelationshipView.as_view(),
70+
name="basic-model-relationships",
71+
),
72+
]
73+
74+
urlpatterns += router.urls

Diff for: tests/test_utils.py

+16
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from rest_framework_json_api import serializers
99
from rest_framework_json_api.utils import (
1010
format_field_names,
11+
format_link_segment,
1112
format_resource_type,
1213
format_value,
1314
get_included_serializers,
@@ -197,6 +198,21 @@ def test_format_field_names(settings, format_type, output):
197198
assert format_field_names(value, format_type) == output
198199

199200

201+
@pytest.mark.parametrize(
202+
"format_type,output",
203+
[
204+
(None, "first_Name"),
205+
("camelize", "firstName"),
206+
("capitalize", "FirstName"),
207+
("dasherize", "first-name"),
208+
("underscore", "first_name"),
209+
],
210+
)
211+
def test_format_field_segment(settings, format_type, output):
212+
settings.JSON_API_FORMAT_LINKS = format_type
213+
assert format_link_segment("first_Name") == output
214+
215+
200216
@pytest.mark.parametrize(
201217
"format_type,output",
202218
[

Diff for: tests/test_views.py

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import pytest
2+
3+
from rest_framework_json_api import serializers, views
4+
from rest_framework_json_api.relations import ResourceRelatedField
5+
from rest_framework_json_api.utils import format_value
6+
7+
from .models import BasicModel
8+
9+
related_model_field_name = "related_field_model"
10+
11+
12+
@pytest.mark.parametrize(
13+
"format_links",
14+
[
15+
None,
16+
"dasherize",
17+
"camelize",
18+
"capitalize",
19+
"underscore",
20+
],
21+
)
22+
def test_get_related_field_name_handles_formatted_link_segments(format_links, rf):
23+
url_segment = format_value(related_model_field_name, format_links)
24+
25+
request = rf.get(f"/basic_models/1/{url_segment}")
26+
27+
view = BasicModelFakeViewSet()
28+
view.setup(request, related_field=url_segment)
29+
30+
assert view.get_related_field_name() == related_model_field_name
31+
32+
33+
class BasicModelSerializer(serializers.ModelSerializer):
34+
related_model_field = ResourceRelatedField(queryset=BasicModel.objects)
35+
36+
def __init__(self, *args, **kwargs):
37+
# Intentionally setting field_name property to something that matches no format
38+
self.related_model_field.field_name = related_model_field_name
39+
super(BasicModelSerializer, self).__init(*args, **kwargs)
40+
41+
class Meta:
42+
model = BasicModel
43+
44+
45+
class BasicModelFakeViewSet(views.ModelViewSet):
46+
serializer_class = BasicModelSerializer
47+
48+
def retrieve(self, request, *args, **kwargs):
49+
pass

0 commit comments

Comments
 (0)