Skip to content

Commit

Permalink
Support formatting URL segments via new FORMAT_LINKS setting
Browse files Browse the repository at this point in the history
  • Loading branch information
platinumazure committed Dec 24, 2020
1 parent 3833271 commit 2bf580e
Show file tree
Hide file tree
Showing 12 changed files with 210 additions and 10 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Jason Housley <[email protected]>
Jerel Unruh <[email protected]>
Jonathan Senecal <[email protected]>
Joseba Mendivil <[email protected]>
Kevin Partington <[email protected]>
Kieran Evans <[email protected]>
Léo S. <[email protected]>
Luc Cary <[email protected]>
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ any parts of the framework not mentioned in the documentation should generally b
### Added

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

### Fixed

Expand Down
38 changes: 38 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,44 @@ When set to pluralize:
}
```

#### Related URL segments

Serializer properties in relationship and related resource URLs may be infected using the `JSON_API_FORMAT_LINKS` setting.

``` python
JSON_API_FORMAT_LINKS = 'dasherize'
```

For example, with a serializer property `created_by` and with `'dasherize'` formatting:

```json{
{
"data": {
"type": "comments",
"id": "1",
"attributes": {
"text": "Comments are fun!"
},
"links": {
"self": "/comments/1"
},
"relationships": {
"created_by": {
"links": {
"self": "/comments/1/relationships/created-by",
"related": "/comments/1/created-by"
}
}
}
},
"links": {
"self": "/comments/1"
}
}
```

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.

### Related fields

#### ResourceRelatedField
Expand Down
10 changes: 5 additions & 5 deletions rest_framework_json_api/relations.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
from rest_framework.serializers import Serializer

from rest_framework_json_api.exceptions import Conflict
from rest_framework_json_api.settings import json_api_settings
from rest_framework_json_api.utils import (
Hyperlink,
format_value,
get_included_serializers,
get_resource_type_from_instance,
get_resource_type_from_queryset,
Expand Down Expand Up @@ -112,13 +114,11 @@ def get_links(self, obj=None, lookup_field="pk"):
else view.kwargs[lookup_field]
}

field_name = self.field_name if self.field_name else self.parent.field_name

self_kwargs = kwargs.copy()
self_kwargs.update(
{
"related_field": self.field_name
if self.field_name
else self.parent.field_name
}
{"related_field": format_value(field_name, json_api_settings.FORMAT_LINKS)}
)
self_link = self.get_url("self", self.self_link_view_name, self_kwargs, request)

Expand Down
1 change: 1 addition & 0 deletions rest_framework_json_api/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
DEFAULTS = {
"FORMAT_FIELD_NAMES": False,
"FORMAT_TYPES": False,
"FORMAT_LINKS": False,
"PLURALIZE_TYPES": False,
"UNIFORM_EXCEPTIONS": False,
}
Expand Down
13 changes: 13 additions & 0 deletions rest_framework_json_api/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,19 @@ def format_resource_type(value, format_type=None, pluralize=None):
return inflection.pluralize(value) if pluralize else value


def format_link_segment(value, format_type=None):
"""
Takes a string value and returns it with formatted keys as set in `format_type`
or `JSON_API_FORMAT_LINKS`.
:format_type: Either 'dasherize', 'camelize', 'capitalize' or 'underscore'
"""
if format_type is None:
format_type = json_api_settings.FORMAT_LINKS

return format_value(value, format_type)


def get_related_resource_type(relation):
from rest_framework_json_api.serializers import PolymorphicModelSerializer

Expand Down
9 changes: 4 additions & 5 deletions rest_framework_json_api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from rest_framework_json_api.utils import (
Hyperlink,
OrderedDict,
format_value,
get_included_resources,
get_resource_type_from_instance,
)
Expand Down Expand Up @@ -185,7 +186,8 @@ def get_related_serializer_class(self):
return parent_serializer_class

def get_related_field_name(self):
return self.kwargs["related_field"]
field_name = self.kwargs["related_field"]
return format_value(field_name, "underscore")

def get_related_instance(self):
parent_obj = self.get_object()
Expand Down Expand Up @@ -227,7 +229,6 @@ class RelationshipView(generics.GenericAPIView):
serializer_class = ResourceIdentifierObjectSerializer
self_link_view_name = None
related_link_view_name = None
field_name_mapping = {}
http_method_names = ["get", "post", "patch", "delete", "head", "options"]

def get_serializer_class(self):
Expand Down Expand Up @@ -400,9 +401,7 @@ def get_related_instance(self):

def get_related_field_name(self):
field_name = self.kwargs["related_field"]
if field_name in self.field_name_mapping:
return self.field_name_mapping[field_name]
return field_name
return format_value(field_name, "underscore")

def _instantiate_serializer(self, instance):
if isinstance(instance, Model) or instance is None:
Expand Down
46 changes: 46 additions & 0 deletions tests/test_relations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import pytest

from rest_framework_json_api.relations import HyperlinkedRelatedField

from .models import BasicModel


@pytest.mark.urls("tests.urls")
@pytest.mark.parametrize(
"format_links,expected_url_segment",
[
(None, "relatedField_name"),
("dasherize", "related-field-name"),
("camelize", "relatedFieldName"),
("capitalize", "RelatedFieldName"),
("underscore", "related_field_name"),
],
)
def test_relationship_urls_respect_format_links(
settings, format_links, expected_url_segment
):
settings.JSON_API_FORMAT_LINKS = format_links

model = BasicModel(text="Some text")

field = HyperlinkedRelatedField(
self_link_view_name="basic-model-relationships",
related_link_view_name="basic-model-related",
read_only=True,
)
field.field_name = "relatedField_name"

expected = {
"self": "/basic_models/{}/relationships/{}/".format(
model.pk,
expected_url_segment,
),
"related": "/basic_models/{}/{}/".format(
model.pk,
expected_url_segment,
),
}

actual = field.get_links(model)

assert expected == actual
16 changes: 16 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from rest_framework_json_api import serializers
from rest_framework_json_api.utils import (
format_field_names,
format_link_segment,
format_resource_type,
format_value,
get_included_serializers,
Expand Down Expand Up @@ -197,6 +198,21 @@ def test_format_field_names(settings, format_type, output):
assert format_field_names(value, format_type) == output


@pytest.mark.parametrize(
"format_type,output",
[
(None, "first_Name"),
("camelize", "firstName"),
("capitalize", "FirstName"),
("dasherize", "first-name"),
("underscore", "first_name"),
],
)
def test_format_field_segment(settings, format_type, output):
settings.JSON_API_FORMAT_LINKS = format_type
assert format_link_segment("first_Name") == output


@pytest.mark.parametrize(
"format_type,output",
[
Expand Down
51 changes: 51 additions & 0 deletions tests/test_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import pytest
from django.test import RequestFactory

from rest_framework_json_api import serializers, views
from rest_framework_json_api.relations import ResourceRelatedField
from rest_framework_json_api.utils import format_value

from .models import BasicModel

related_model_field_name = "related_field_model"


@pytest.mark.urls("tests.urls")
@pytest.mark.parametrize(
"format_links",
[
None,
"dasherize",
"camelize",
"capitalize",
"underscore",
],
)
def test_get_related_field_name_handles_formatted_link_segments(format_links):
url_segment = format_value(related_model_field_name, format_links)

request = RequestFactory().get("/basic_models/1/{}".format(url_segment))

view = BasicModelFakeViewSet()
view.setup(request, related_field=url_segment)

assert view.get_related_field_name() == related_model_field_name


class BasicModelSerializer(serializers.ModelSerializer):
related_model_field = ResourceRelatedField(queryset=BasicModel.objects)

def __init__(self, *args, **kwargs):
# Intentionally setting field_name property to something that matches no format
self.related_model_field.field_name = related_model_field_name
super(BasicModelSerializer, self).__init(*args, **kwargs)

class Meta:
model = BasicModel


class BasicModelFakeViewSet(views.ModelViewSet):
serializer_class = BasicModelSerializer

def retrieve(self, request, *args, **kwargs):
pass
22 changes: 22 additions & 0 deletions tests/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from django.conf.urls import re_path
from rest_framework.routers import SimpleRouter

from .views import BasicModelRelationshipView, BasicModelViewSet

router = SimpleRouter()
router.register(r"basic_models", BasicModelViewSet, basename="basic-model")

urlpatterns = [
re_path(
r"^basic_models/(?P<pk>[^/.]+)/(?P<related_field>[^/.]+)/$",
BasicModelViewSet.as_view({"get": "retrieve_related"}),
name="basic-model-related",
),
re_path(
r"^basic_models/(?P<pk>[^/.]+)/relationships/(?P<related_field>[^/.]+)/$",
BasicModelRelationshipView.as_view(),
name="basic-model-relationships",
),
]

urlpatterns += router.urls
12 changes: 12 additions & 0 deletions tests/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from rest_framework_json_api.views import ModelViewSet, RelationshipView

from .models import BasicModel


class BasicModelViewSet(ModelViewSet):
class Meta:
model = BasicModel


class BasicModelRelationshipView(RelationshipView):
queryset = BasicModel.objects

0 comments on commit 2bf580e

Please sign in to comment.