Skip to content

Support formatting URL segments via new FORMAT_LINKS setting #876

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Dec 27, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
11 changes: 4 additions & 7 deletions rest_framework_json_api/relations.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from rest_framework_json_api.exceptions import Conflict
from rest_framework_json_api.utils import (
Hyperlink,
format_link_segment,
get_included_serializers,
get_resource_type_from_instance,
get_resource_type_from_queryset,
Expand Down Expand Up @@ -112,14 +113,10 @@ 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
}
)
self_kwargs.update({"related_field": format_link_segment(field_name)})
self_link = self.get_url("self", self.self_link_view_name, self_kwargs, request)

# Assuming RelatedField will be declared in two ways:
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
4 changes: 3 additions & 1 deletion 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
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