Skip to content

Commit 2bf580e

Browse files
committed
Support formatting URL segments via new FORMAT_LINKS setting
Fixes #790.
1 parent 3833271 commit 2bf580e

File tree

12 files changed

+210
-10
lines changed

12 files changed

+210
-10
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

+5-5
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
from rest_framework.serializers import Serializer
1414

1515
from rest_framework_json_api.exceptions import Conflict
16+
from rest_framework_json_api.settings import json_api_settings
1617
from rest_framework_json_api.utils import (
1718
Hyperlink,
19+
format_value,
1820
get_included_serializers,
1921
get_resource_type_from_instance,
2022
get_resource_type_from_queryset,
@@ -112,13 +114,11 @@ def get_links(self, obj=None, lookup_field="pk"):
112114
else view.kwargs[lookup_field]
113115
}
114116

117+
field_name = self.field_name if self.field_name else self.parent.field_name
118+
115119
self_kwargs = kwargs.copy()
116120
self_kwargs.update(
117-
{
118-
"related_field": self.field_name
119-
if self.field_name
120-
else self.parent.field_name
121-
}
121+
{"related_field": format_value(field_name, json_api_settings.FORMAT_LINKS)}
122122
)
123123
self_link = self.get_url("self", self.self_link_view_name, self_kwargs, request)
124124

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

+4-5
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()
@@ -227,7 +229,6 @@ class RelationshipView(generics.GenericAPIView):
227229
serializer_class = ResourceIdentifierObjectSerializer
228230
self_link_view_name = None
229231
related_link_view_name = None
230-
field_name_mapping = {}
231232
http_method_names = ["get", "post", "patch", "delete", "head", "options"]
232233

233234
def get_serializer_class(self):
@@ -400,9 +401,7 @@ def get_related_instance(self):
400401

401402
def get_related_field_name(self):
402403
field_name = self.kwargs["related_field"]
403-
if field_name in self.field_name_mapping:
404-
return self.field_name_mapping[field_name]
405-
return field_name
404+
return format_value(field_name, "underscore")
406405

407406
def _instantiate_serializer(self, instance):
408407
if isinstance(instance, Model) or instance is None:

Diff for: tests/test_relations.py

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import pytest
2+
3+
from rest_framework_json_api.relations import HyperlinkedRelatedField
4+
5+
from .models import BasicModel
6+
7+
8+
@pytest.mark.urls("tests.urls")
9+
@pytest.mark.parametrize(
10+
"format_links,expected_url_segment",
11+
[
12+
(None, "relatedField_name"),
13+
("dasherize", "related-field-name"),
14+
("camelize", "relatedFieldName"),
15+
("capitalize", "RelatedFieldName"),
16+
("underscore", "related_field_name"),
17+
],
18+
)
19+
def test_relationship_urls_respect_format_links(
20+
settings, format_links, expected_url_segment
21+
):
22+
settings.JSON_API_FORMAT_LINKS = format_links
23+
24+
model = BasicModel(text="Some text")
25+
26+
field = HyperlinkedRelatedField(
27+
self_link_view_name="basic-model-relationships",
28+
related_link_view_name="basic-model-related",
29+
read_only=True,
30+
)
31+
field.field_name = "relatedField_name"
32+
33+
expected = {
34+
"self": "/basic_models/{}/relationships/{}/".format(
35+
model.pk,
36+
expected_url_segment,
37+
),
38+
"related": "/basic_models/{}/{}/".format(
39+
model.pk,
40+
expected_url_segment,
41+
),
42+
}
43+
44+
actual = field.get_links(model)
45+
46+
assert expected == actual

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

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

Diff for: tests/urls.py

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from django.conf.urls import re_path
2+
from rest_framework.routers import SimpleRouter
3+
4+
from .views import BasicModelRelationshipView, BasicModelViewSet
5+
6+
router = SimpleRouter()
7+
router.register(r"basic_models", BasicModelViewSet, basename="basic-model")
8+
9+
urlpatterns = [
10+
re_path(
11+
r"^basic_models/(?P<pk>[^/.]+)/(?P<related_field>[^/.]+)/$",
12+
BasicModelViewSet.as_view({"get": "retrieve_related"}),
13+
name="basic-model-related",
14+
),
15+
re_path(
16+
r"^basic_models/(?P<pk>[^/.]+)/relationships/(?P<related_field>[^/.]+)/$",
17+
BasicModelRelationshipView.as_view(),
18+
name="basic-model-relationships",
19+
),
20+
]
21+
22+
urlpatterns += router.urls

Diff for: tests/views.py

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from rest_framework_json_api.views import ModelViewSet, RelationshipView
2+
3+
from .models import BasicModel
4+
5+
6+
class BasicModelViewSet(ModelViewSet):
7+
class Meta:
8+
model = BasicModel
9+
10+
11+
class BasicModelRelationshipView(RelationshipView):
12+
queryset = BasicModel.objects

0 commit comments

Comments
 (0)