Could not resolve URL for hyperlinked relationship using view name "<viewname>". ResourceRelatedField URL/pk problem (django+rest+json api)
Summary
Background
I am trying to implement a JSON:API-compliant general Django app to be used in various projects. I am able to generate decent JSON:API, but I only get "related"-links for one-to-one relationships (foreignkey in the model). I cannot get "related"-links to reverse relationships (other models with a foreign key pointing to my object).
Thank you! //S
Questions
- Is ResourceRelatedField the correct way to implement reverse relationship links (e.g. "related")?
- How do you properly use ResourceRelatedField with related_link_view_name and _url_kwarg?
Specifics
- I use Django on a ubuntu server (local network) with Django restful framework and Django restful framework json api.
- There are three models in the database: TopObject, Status and RelatedObject.
- TopObject-objects has a foreignkey status pointing to Status
- RelatedObject-objects has a foreignkey topobject pointing to its 'parent' TopObject-object.
- All of the following url's work properly when entering them manually (e.g. browser or httpie):
- localhost:8000/topobjects/1/relatedobjects/ (gives a list of related objects with topobject foreignkey = 1)
- *localhost:8000/topobjects/ (gives list, currently without reverse links. The related link for Status [direct relationship] works).
- *localhost:8000/topobjects/1/ (gives topobject 1, currently without reverse links. The related link for status [direct relationship] works).
- localhost:8000/relatedobjects/
- localhost:8000/relatedobjects/1/
- localhost:8000/statuses
- localhost:8000/statuses/1/
- The view name topobject-relatedobject-list works from views when tested with print(reverse...)
- When related_link_view_name='topobject-relatedobject-list' is set under ResourceRelatedField, localhost:8000/relatedobjects/ and localhost:8000/relatedobjects/1/ raises a Django error.
- I have not figured out how to amend reverse relationship lists within routers/viewsets. Unless that has specific bearing on the problem at hand, I will treat that as a separate issue for later.
Supporting information
Target JSON output
"relationships": {
"relatedobjects": {
"meta": {
"count": 2
},
"data": [
{
"type": "relatedobject",
"id": "1"
},
{
"type": "relatedobject",
"id": "2"
}
],
//***************MISSING**************//
"links": {
"related": "http://192.168.0.152:8000/topobjects/1/relatedobjects"
}
//***************END MISSING**************//
},
"status": {
"data": {
"type": "status",
"id": "1"
},
"links": {
"related": "http://192.168.0.152:8000/statuses/1/"
}
}
},
Django error page
ImproperlyConfigured at /topobjects/2/
Could not resolve URL for hyperlinked relationship using view name "topobject-relatedobject-list".
Request Method: GET
Request URL: http://192.168.0.152:8000/topobjects/2/
Django Version: 3.2.9
Exception Type: ImproperlyConfigured
Exception Value:
Could not resolve URL for hyperlinked relationship using view name "topobject-relatedobject-list".
Exception Location: /django/api2/env/lib/python3.8/site-packages/rest_framework_json_api/relations.py, line 98, in get_url
Python Executable: /django/api2/env/bin/python
Python Version: 3.8.10
Python Path:
['/django/api2',
'/usr/lib/python38.zip',
'/usr/lib/python3.8',
'/usr/lib/python3.8/lib-dynload',
'/django/api2/env/lib/python3.8/site-packages']
Server time: Tue, 07 Dec 2021 13:46:48 +0000
models.py
from django.db import models
class TopObject(models.Model):
created = models.DateTimeField(auto_now_add=True)
title = models.CharField(max_length=100, blank=True, default='')
status = models.ForeignKey('Status', default=1, on_delete=models.CASCADE)
content = models.TextField(blank=True)
def __str__(self):
return str(self.pk) + ': ' + self.title + ' (' + str(self.status.name) + ')'
class JSONAPIMeta:
resource_name = 'topobject'
class Status(models.Model):
name = models.CharField(max_length=100)
description = models.TextField(blank=True)
def __str__(self):
return str(self.pk) + ': ' + self.name
class JSONAPIMeta:
resource_name = 'status'
class RelatedObject(models.Model):
topobject = models.ForeignKey('TopObject', blank=True, related_name='relatedobjects', null=True, on_delete=models.CASCADE)
title = models.CharField(max_length=100, blank=True)
a = models.BooleanField(default=False)
b = models.BooleanField(default=False)
def __str__(self):
return str(self.pk) + ': ' + self.title
class JSONAPIMeta:
resource_name = 'relatedobject'
serializers.py
from rest_framework_json_api import serializers
from .models import TopObject, Status, RelatedObject
from rest_framework_json_api.relations import ResourceRelatedField
class RelatedObjectSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = RelatedObject
fields = '__all__'
class TopObjectSerializer(serializers.HyperlinkedModelSerializer):
related_serializers = {'relatedobjects': 'api.serializers.RelatedObjectSerializer', 'status': 'api.serializers.StatusSerializer'}
relatedobjects = ResourceRelatedField(
queryset=RelatedObject.objects,
many=True,
related_link_view_name='topobject-relatedobject-list',
related_link_url_kwarg='pk'
)
class Meta:
model = TopObject
fields = '__all__'
extra_fields = ['relatedobjects', 'testrelated']
class StatusSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Status
fields ='__all__'
views.py
from .models import TopObject, Status, RelatedObject
from .serializers import TopObjectSerializer, StatusSerializer, RelatedObjectSerializer
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
class TopObjectViewSet(viewsets.ModelViewSet):
queryset = TopObject.objects.all()
serializer_class = TopObjectSerializer
class StatusViewSet(viewsets.ModelViewSet):
queryset = Status.objects.all()
serializer_class = StatusSerializer
class RelatedObjectViewSet(viewsets.ModelViewSet):
queryset = RelatedObject.objects.all()
serializer_class = RelatedObjectSerializer
class TopObjectRelatedObjectViewSet(viewsets.ModelViewSet):
serializer_class = RelatedObjectSerializer
def get_queryset(self):
queryset = RelatedObject.objects.all().filter(topobject=self.kwargs['pk'])
return queryset
def perform_create(self, serializer):
topobject = TopObject.objects.get(pk=self.kwargs['pk'])
serializer.save(topobject=topobject)
urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views
from django.conf.urls import url
router = DefaultRouter()
router.register(r'topobjects', views.TopObjectViewSet)
router.register(r'statuses', views.StatusViewSet)
router.register(r'relatedobjects', views.RelatedObjectViewSet)
topobjectsrelatedobjects = views.TopObjectRelatedObjectViewSet.as_view({'get': 'list', 'post': 'create'})
urlpatterns = [
path('topobjects/<int:pk>/relatedobjects/', topobjectsrelatedobjects, name='topobject-relatedobject-list'),
path('', include(router.urls)),
]
settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework_json_api',
'api',
]
REST_FRAMEWORK = {
'PAGE_SIZE': 2,
'EXCEPTION_HANDLER': 'rest_framework_json_api.exceptions.exception_handler',
'DEFAULT_PAGINATION_CLASS':
'rest_framework_json_api.pagination.JsonApiPageNumberPagination',
'DEFAULT_PARSER_CLASSES': (
'rest_framework_json_api.parsers.JSONParser',
'rest_framework.parsers.FormParser',
'rest_framework.parsers.MultiPartParser'
),
'DEFAULT_RENDERER_CLASSES': (
'rest_framework_json_api.renderers.JSONRenderer',
'rest_framework_json_api.renderers.BrowsableAPIRenderer'
),
'DEFAULT_METADATA_CLASS': 'rest_framework_json_api.metadata.JSONAPIMetadata',
'DEFAULT_SCHEMA_CLASS': 'rest_framework_json_api.schemas.openapi.AutoSchema',
'DEFAULT_FILTER_BACKENDS': (
'rest_framework_json_api.filters.QueryParameterValidationFilter',
'rest_framework_json_api.filters.OrderingFilter',
'rest_framework_json_api.django_filters.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
),
'SEARCH_PARAM': 'filter[search]',
'TEST_REQUEST_RENDERER_CLASSES': (
'rest_framework_json_api.renderers.JSONRenderer',
),
'TEST_REQUEST_DEFAULT_FORMAT': 'vnd.api+json',
}
- Is ResourceRelatedField the correct way to implement reverse relationship links (e.g. "related")?
Yes. This is the standard in Django REST json api. - How do you properly use ResourceRelatedField with related_link_view_name and _url_kwarg?
As the framework uses the queryset model name (i.e. 'RelatedObject') as a basis for the kwarg called related_fields, the url.conf must accept the following:
path('topobjects/<int:pk>/<related_field>/', views.TopObjectView.as_view(), name='topobject-relatedobject-list'),
The following url.conf will not work in this example:
path('topobjects/<int:pk>/relatedobjects/', views.TopObjectView.as_view(), name='topobject-relatedobject-list'),
The forward url would work in the second example, but not the reverse that ResourceRelatedField relies on.