Why can't Django REST Framework's HyperlinkedModelSerializer form URL? - django

New to DRF and everything works as long as I don't include 'url' in fields. Here's what I've got:
Serializer:
class TaskSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Task
fields = ('pk', 'short_desc', 'scheduled_date')
View Set:
class TaskViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Task.objects.all().order_by('scheduled_date')
serializer_class = TaskSerializer
URLs:
router = routers.DefaultRouter()
router.register(r'tasks', views.TaskViewSet)
urlpatterns = [
[... bunch of non-REST URLs]
# REST API
url(r'^', include(router.urls)),
At runtime, printing router.urls gives me:
<RegexURLPattern api-root ^$>
<RegexURLPattern api-root ^\.(?P<format>[a-z0-9]+)/?$>
<RegexURLPattern task-list ^tasks/$>
<RegexURLPattern task-list ^tasks\.(?P<format>[a-z0-9]+)/?$>
<RegexURLPattern task-detail ^tasks/(?P<pk>[^/.]+)/$>
<RegexURLPattern task-detail ^tasks/(?P<pk>[^/.]+)\.(?P<format>[a-z0-9]+)/?$>
Both [...]/tasks/ and [...]/tasks/123/ work if I type them into my browser, which leads me to believe that task-list and task-detail views do, in fact exist.
Now I introduce a problem by adding 'url' to the serializer:
class TaskSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Task
fields = ('url', 'pk', 'short_desc', 'scheduled_date')
After adding 'url' to fields, I get the following error:
Could not resolve URL for hyperlinked relationship using view name
"task-detail". You may have failed to include the related model in
your API, or incorrectly configured the lookup_field attribute on
this field.
The DRF docs say:
There needs to be a way of determining which views should be used for
hyperlinking to model instances. By default hyperlinks are expected to
correspond to a view name that matches the style '{model_name}-detail',
and looks up the instance by a pk keyword argument.
Since I've verified that task-detail exists and that the corresponding URL [...]/tasks/123/ works, I can't for the life of me figure out why DRF can't form the URL. Any ideas?

Inspired by clues revealed by Kunkka's answer, I have a solution that looks like this:
class TaskSerializer(serializers.HyperlinkedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name="task:task-detail")
class Meta:
model = Task
fields = ('url', 'pk', 'short_desc', 'scheduled_date')
I.e. I've added an url = [...] line to the serializer I originally posted in my question. This solves the lookup problem which was presumably caused by DRF not knowing that 'task-detail' is actually in the 'task' namespace.
Any better solutions?

Can you try this?
class TaskSerializer(serializers.HyperlinkedModelSerializer):
url = serializers.SerializerMethodField()
class Meta:
model = Task
fields = ('pk','url', 'short_desc', 'scheduled_date')
def get_url(self,obj):
request = self.context['request']
return = {'self':reverse('task-detail',kwargs={'pk':obj.pk},request=request)}

Related

Django REST Framework; ImproperlyConfigured; Could not resolve URL for hyperlinked relationship

Am trying to use DRF to make an election tracker; getting on what was supposed to be fairly simple ViewSet list and retrieve endpoints;
django.core.exceptions.ImproperlyConfigured: Could not resolve URL for hyperlinked relationship using view name "election-detail". You may have failed to include the related model in your API, or incorrectly configured the `lookup_field` attribute on this field.
Model Snippet...
class Election(models.Model):
_id = models.CharField(
name="id", primary_key=True, validators=[election_ids.validate], max_length=32
)
date = ComputedDateField(compute_from="_date") # not relevant
parent = models.ForeignKey(
to="self", on_delete=models.CASCADE, default=None, null=True
)
Serializers
class ElectionSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Election
# Removing the 'url' from below stops the ImproperlyConfigured error on retrieve calls
fields = ["url", "org", "date", "constituency", "parent"]
read_only_fields = ["url"]
depth=1
ViewSet
class ElectionViewSet(viewsets.ReadOnlyModelViewSet):
"""
API endpoint that allows elections to be viewed or edited.
"""
lookup_field = 'id'
lookup_value_regex = '[a-z0-9.\-_]+' # this might be the culprit, see below
queryset = Election.objects.all().order_by("-date")
serializer_class = ElectionSerializer
permission_classes = [permissions.AllowAny]
and relevant urls.py
app_name = "counts"
api_router = routers.DefaultRouter()
api_router.register(r"users", UserViewSet, "users")
api_router.register(r"groups", GroupViewSet, "groups")
api_router.register(r"elections", ElectionViewSet, "elections")
api_router.register(r"candidates", CandidateViewSet, "candidates")
api_router.register(r"stages", StageViewSet, "stages")
# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API.
urlpatterns = [
path("", TemplateView.as_view(template_name="home.html"), name='home'),
path("about/", TemplateView.as_view(template_name="about.html"), name='about'),
path("api/v1/", include(api_router.urls)),
path("api-auth/", include("rest_framework.urls", namespace="rest_framework")),
]
I think the issue is that the ID's are in a standard format that contains periods, which aren't a standard part of the SlugField type, and my working hunch is that something in the bowels of HyperlinkedModelSerializer is having trouble building the reverse / parent entity urls.
If I remove the "url" from ElectionSerializer.Meta.fields, then the individual 'election-detail' view appears to work fine (i.e. I can get a successful, correct, response on /api/v1/elections/nia.2016-05-05/) but I still can't get a listing going on .../elections/.
My understanding was that ReadOnlyModelViewSet would look after the default routing and view generation for list and retrieve operations which at this point are the only ones I'm interested in.
Things I have tried:
Adding Namespaces and Names to the urls in both / and the base urls or the app-level urls
Changing the model to use a custom 'slug' field as the lookup with s/\./_/g
Hopefully not whatever you're about to suggest.
Any ideas? (Full codebase here for anyone feeling masochistic.)

django rest framework access item by lookup field instead of pk 3.4 DRF

I need to have lookup field in order my frontend sends email which should be deleted but I get item not found. I've researched a lot about this problem but I can't figure out which DRF version what supports.
class EmailReminderSerializer(serializers.ModelSerializer):
city = serializers.CharField(max_length=255)
url = serializers.HyperlinkedIdentityField(
view_name='web:email_reminder-detail',
)
class Meta:
model = EmailReminder
fields = '__all__'
extra_kwargs = {
'url': {'lookup_field': 'email'}
}
Now I have url but it points to instance pk, not by my desired lookup field.
Any suggestions of how it works in 3.4 version or do you have any other solutions to some lower version >=3.0?
Oh okay, I got it. For serialized models you only need lookup_field in your view but for hyperlinked serialized models you need extra_kwargs in serializers plus lookup field in views. Hope it helps someone
You should modify the lookup field in your view instead. As shown in DRF docs, you can do the following.
in views.py
from rest_framework import viewsets
class EmailReminderViewSet(viewsets.ModelViewSet):
serializer_class = TagSerializer
lookup_field = 'email'

how to troubleshoot DRF router duplicate urls

I am trying to set a series of Django Rest Framework URLs.
Below is my Serializer/ViewSet makeup
class ModelSerializer(serializers.HyperlinkedModelSerializer):
schemas = SchemaSerializer(many=True, read_only=True)
class Meta:
model = dbModels
fields = ('ModelName', 'pk', 'schemas')
class ModelViewSet(viewsets.ModelViewSet):
queryset = dbModels.objects.all()
serializer_class = ModelSerializer
class ModelListSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = dbModels
fields = ('ModelName', 'pk')
class ModelListViewSet(viewsets.ModelViewSet):
queryset = dbModels.objects.all()
serializer_class = ModelListSerializer
Here is my Router List:
from datagenerator.serializer import UserViewSet, \
ModelViewSet, ModelListViewSet
router = routers.DefaultRouter()
router.register(r'models', ModelViewSet)
router.register(r'modellist', ModelListViewSet)
However, when I'm running The localhost webserver, the DRF Front end shows this:
"models": "http://localhost:8000/datamaker/api/modellist/",
"modellist": "http://localhost:8000/datamaker/api/modellist/",
How do I stop this?
I need models to go to models and modellist to go to modellist.
Thanks much...
Use the base_name argument:
router.register(r'models', ModelViewSet, base_name='models')
router.register(r'modellist', ModelListViewSet, base_name='modellist')
Since your serializers share the same data model, DRF might get stuck trying to automatically discover the url naming pattern. So it's better in this case to explicitly set the base_name.
If you're using a newer version of Django Rest Framework, you'll need to use basename='models' instead of base_name='model'.

django rest extensions drf-extensions TypeError at /data/ register() got an unexpected keyword argument 'parents_query_lookups'

I am trying to get a simple nested route set up with drf-extensions but am having trouble following the docs, I am getting this error:
TypeError at /data/
register() got an unexpected keyword argument 'parents_query_lookups'
Trying to achieve /data/Survey/<survey>/Version/<version>/Product/<product>/
each survey has multiple versions and those versions will contain multiple Products e.g., /data/survey/survey1_name/version/survey1_version1/product/survey1_version_product1/
but currently I have un-nested endpoints
/data/survey/
/data/versions/
/data/products/
models.py
class Survey(models.Model):
survey = models.CharField(choices=SURVEYS, max_length=100)
class Version(models.Model):
version = models.CharField(choices=DATA_RELEASES, max_length=50)
survey = models.ForeignKey(Survey)
class Product(models.Model):
product = models.CharField(choices=PRODUCTS, max_length=100)
version = models.ForeignKey(Version)
views.py
class SurveyViewSet(NestedViewSetMixin, viewsets.ModelViewSet):
queryset = Survey.objects.all()
serializer_class = SurveySerializer
permission_classes = [permissions.AllowAny]
class VersionViewSet(NestedViewSetMixin, viewsets.ModelViewSet):
queryset = Version.objects.all()
serializer_class = VersionSerializer
permission_classes = [permissions.AllowAny]
class ProductViewSet(NestedViewSetMixin, viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
permission_classes = [permissions.AllowAny]
serializers.py
class SurveySerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Survey
fields = ('id', 'survey')
class VersionSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Version
fields = ('id', 'version', 'survey')
class ProductSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Product
fields = ('id', 'product', 'version')
urls.py
router = ExtendDefaultRouter()
router.register(r'surveys', views.SurveyViewSet, base_name='survey')
router.register(r'versions', views.VersionViewSet, parents_query_lookups=['survey']),
router.register(r'products', views.ProductViewSet, parents_query_lookups=['version'])
urlpatterns = [
url(r'^data/', include(router.urls)),
]
You should chain the calls to register(), instead of calling them directly on the router :
urls.py
router = ExtendDefaultRouter()
(router.register(r'surveys', views.SurveyViewSet, base_name='survey')
.register(r'versions', views.VersionViewSet, parents_query_lookups=['survey']),
.register(r'products', views.ProductViewSet, parents_query_lookups=['version']))
That's because NestedRouterMixin.register() returns an instance of NestedRegistryItem, which is the class that understand the parents_query_lookups parameter.
In order to keep your project well organized in accordance with the hierarchy of your data, the first thing I would do is separate each tier of urls into a separate entity, and possibly even a separate file. The second would be to add an primary-key endpoint to each tier of the rest API. In the end it would look something like this:
survey_urls.py
router.register(r'survey', views.SurveyViewSet, base_name='survey')
router.register(r'survey-version/', include('my_app.rest_server.version_urls'))
version_urls.py
router.register(r'version', views.VersionViewSet, base_name='version')
router.register(r'version-product/', include('my_app.rest_server.product_urls'))
product_urls.py
router.register(r'product/(?P<pk>[0-9]+)$', views.ProductViewSet, base_name='product')
Then, your final url could look like: /data/survey-version/33/version-product/5/product/8
In all likelihood, this is an unnecessary complication of things, since each product has its own unique id, and then you can just access it with a url like: /data/product/pk, using a single url routing, namely the last line of code above.
My examples assumed the records have a numeric primary key, if the case is otherwise you'd have to change the regex accordingly.

How to represent `self` url in django-rest-framework

I want to add a link to a single resource representation which is an URL to itself, self. Like (taken from documentation):
class AlbumSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Album
fields = ('album_name', 'artist', 'track_listing')
{
'album_name': 'The Eraser',
'artist': 'Thom Yorke',
'self': 'http://www.example.com/api/album/2/',
}
How should this be done?
If you inherit serializers.HyperlinkedModelSerializer all you need to do is pass a url field to fields. See the docs here:
http://www.django-rest-framework.org/tutorial/5-relationships-and-hyperlinked-apis/
Alright, this solved my problem but if you have a better solution please post an answer:
from django.urls import reverse
from rest_framework import serializers
self_url = serializers.SerializerMethodField('get_self')
def get_self(self, obj):
request = self.context['request']
return reverse('album-detail', kwargs={'id': obj.id}, request=request)
here is my solution,
in your view methods create serilizer object like this:
album = AlbumSerializer(data=data, {"request":request})
in your serilizer class override to_representation method (you can read about this method on DRF docs
class AlbumSerializer(serializers.HyperlinkedModelSerializer):
def to_representation(self, obj):
data = super().to_representation(obj)
request = self.context["request"]
return data
According to this issue, you can just add 'url' in the list of fields.
Here is a little more context than you got in the other answers so far. The key is the context argument passed to the serializer constructor and the 'url' in fields.
http://www.django-rest-framework.org/tutorial/5-relationships-and-hyperlinked-apis/
In your viewset:
class AlbumViewSet(viewsets.ViewSet):
def list(self, request):
queryset = Album.objects.all()
serializer = AlbumSerializer(queryset, many=True,
context={'request': request})
return Response(serializer.data)
In the serializer:
class AlbumSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Album
fields = ('album_name', 'artist', 'track_listing', 'url')
As stated above the HyperlinkedModelSerializer will convert all your related fields and remove the ID of the resource as well. I use one of the following solutions, depending on the situation.
Solution 1: import api settings and add the url field:
For more details see URL_FIELD_NAME.
from rest_framework.settings import api_settings
class AlbumSerializer(SelfFieldMixin, serializers.ModelSerializer):
class Meta:
model = Album
fields = ('album_name', 'artist', 'track_listing', api_settings.URL_FIELD_NAME)
Solution 2: a simple mixin, which only works if default fields are used:
class SelfFieldMixin:
"""
Adds the self link without converting all relations to HyperlinkedRelatedField
"""
def get_default_field_names(self, declared_fields, model_info):
"""
Return the default list of field names that will be used if the
`Meta.fields` option is not specified.
"""
default_fields = super().get_default_field_names(declared_fields, model_info)
return [self.url_field_name, *default_fields]
And it can be used like
class AlbumSerializer(SelfFieldMixin, serializers.ModelSerializer):
class Meta:
model = Album
fields = '__all__'
NOTE: It requires Python 3 due to the super() call, the a mixin must be placed before any of the serializer classes!
P.S.: To achieve the required response in the question one must also set the URL_FIELD_NAME to 'self'.
Edit: get_default_field_names must return a list object for Meta.exclude to work on ModelSerializers.
You can use the HyperlinkedIdentityField like so:
class ThingSerializer(ModelSerializer):
class Meta:
model = Thing
fields = ['self_link', ...]
self_link = HyperlinkedIdentityField(view_name='thing-detail')
You need to have your routes named appropriately but the Default routers do so automatically (documented here).
As others have pointed out the HyperlinkedModelSerializer also works. This is because it uses this field automatically. See here.