DRF Could not resolve URL for hyperlinked relationship using view name on PrimaryKeyRelatedField
Asked Answered
M

1

2

I have a frustrating problem with POST requests on a DRF serializer - DRF is, for some reason, going to an incorrect view name, and view_name is not a settable property on PrimaryKeyRelated Field.

Models:

# (the class with the issue)
class Section(models.Model):
    teacher = models.ManyToManyField(Teacher)

# (a class that works, using the same pattern)
class Assessment(models.Model):
    standards = models.ManyToManyField(Standard)

Serializers:

# (doesn't work)
class SectionInfoSerializer(serializers.HyperlinkedModelSerializer):
    url = serializers.HyperlinkedIdentityField(view_name="gbook:section-detail")
    teacher = serializers.PrimaryKeyRelatedField(many=True, read_only=True),
    teachers_id = serializers.PrimaryKeyRelatedField(write_only=True, queryset=Teacher.objects.all(), many=True, source='teacher', allow_empty=False)

    class Meta:
        model = Section
        fields = '__all__'
        read_only_fields = ['sendEmails', 'teacher', 'course']

# (works)
class AssessmentSerializer(serializers.HyperlinkedModelSerializer):
    pk = serializers.PrimaryKeyRelatedField(read_only=True)
    url = serializers.HyperlinkedIdentityField(view_name="appname:assessments-detail")
    standards = serializers.PrimaryKeyRelatedField(read_only=True, many=True)
    standards_id = serializers.PrimaryKeyRelatedField(queryset=Standard.objects.all(), source='standards', write_only=True, many=True, allow_empty=False)

    class Meta:
        model = Assessment
        fields = '__all__'

urls:

router.register(r'teachers', teacher_views.TeacherViewSet, basename='teacher')
router.register(r'sections', course_views.SectionViewSet)
router.register(r'standards', gbook.views.standard_views.StandardViewSet, basename='standards')
router.register(r'assessments', AssessmentViewSet, basename='assessments')

I'm using the _id fields during POST and PUT to send the id's of the related obejcts, then serializing them. This worked great with AssessmentSerializer (and several others), but is failing for a reason that I can't figure out. Certainly, the appname is missing from the view returned in the error, but I don't know why that's happening, and why it didn't happen before.

Stack trace:

Internal Server Error: /appname/sections/
Traceback (most recent call last):
  File "/venv2/lib/python3.8/site-packages/rest_framework/relations.py", line 393, in to_representation
    url = self.get_url(value, self.view_name, request, format)
  File "/venv2/lib/python3.8/site-packages/rest_framework/relations.py", line 331, in get_url
    return self.reverse(view_name, kwargs=kwargs, request=request, format=format)
  File "/venv2/lib/python3.8/site-packages/rest_framework/reverse.py", line 47, in reverse
    url = _reverse(viewname, args, kwargs, request, format, **extra)
  File "/venv2/lib/python3.8/site-packages/rest_framework/reverse.py", line 60, in _reverse
    url = django_reverse(viewname, args=args, kwargs=kwargs, **extra)
  File "/venv2/lib/python3.8/site-packages/django/urls/base.py", line 87, in reverse
    return iri_to_uri(resolver._reverse_with_prefix(view, prefix, *args, **kwargs))
  File "/venv2/lib/python3.8/site-packages/django/urls/resolvers.py", line 685, in _reverse_with_prefix
    raise NoReverseMatch(msg)
django.urls.exceptions.NoReverseMatch: Reverse for 'teacher-detail' not found. 'teacher-detail' is not a valid view function or pattern name.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/lib/python3.8/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/lib/python3.8/site-packages/django/core/handlers/base.py", line 179, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/lib/python3.8/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
    return view_func(*args, **kwargs)
  File "/lib/python3.8/site-packages/rest_framework/viewsets.py", line 114, in view
    return self.dispatch(request, *args, **kwargs)
  File "/lib/python3.8/site-packages/rest_framework/views.py", line 505, in dispatch
    response = self.handle_exception(exc)
  File "/lib/python3.8/site-packages/rest_framework/views.py", line 465, in handle_exception
    self.raise_uncaught_exception(exc)
  File "/lib/python3.8/site-packages/rest_framework/views.py", line 476, in raise_uncaught_exception
    raise exc
  File "/lib/python3.8/site-packages/rest_framework/views.py", line 502, in dispatch
    response = handler(request, *args, **kwargs)
  File "/lib/python3.8/site-packages/rest_framework/mixins.py", line 20, in create
    headers = self.get_success_headers(serializer.data)
  File "/lib/python3.8/site-packages/rest_framework/serializers.py", line 562, in data
    ret = super().data
  File "/lib/python3.8/site-packages/rest_framework/serializers.py", line 260, in data
    self._data = self.to_representation(self.instance)
  File "/lib/python3.8/site-packages/rest_framework/serializers.py", line 529, in to_representation
    ret[field.field_name] = field.to_representation(attribute)
  File "/lib/python3.8/site-packages/rest_framework/relations.py", line 533, in to_representation
    return [
  File "/lib/python3.8/site-packages/rest_framework/relations.py", line 534, in <listcomp>
    self.child_relation.to_representation(value)
  File "/lib/python3.8/site-packages/rest_framework/relations.py", line 408, in to_representation
    raise ImproperlyConfigured(msg % self.view_name)
django.core.exceptions.ImproperlyConfigured: Could not resolve URL for hyperlinked relationship using view name "teacher-detail". You may have failed to include the related model in your API, or incorrectly configured the `lookup_field` attribute on this field.
[24/Dec/2020 09:55:04] "POST /appname/sections/ HTTP/1.1" 500 157269
Madrigal answered 24/12, 2020 at 16:2 Comment(11)
I am almost sure that you don't have a view/url named teacher-detail but possibly some_app_name:teacher-detail. You can list all URL patterns in your project by using this method, and use a grep expression to see what is the actual URL nameLodovico
Correct - teacher-detail isn't a view: should be appname:teacher-detail. However, there's no way to set the correct view name on that field that I know ofMadrigal
There is a teachers_id (plural), but a teacher field (singular). Copy/paste error or real issue?Vicenta
You can use a HyperlinkedRelatedField with view_name parameter @MadrigalLodovico
@Lodovico There's no reason for a PrimaryKeyRelatedField to need a view. That's the problem. It should just stick the id in the database field.Vicenta
@Melvyn: neither - the field name teachers_id is irrelevant. The real history is that the legacy DB I'm working with has a poorly-named field (teacher), but it shouldn't be an issue hereMadrigal
But why does it want teacher-detail? Is that defined in BasicTeacherSerializer? The code you're showing has section-detail and assessmentsdetail. There is no reason to ask for teacher-detail.Vicenta
Put it under debugger, set a breakpoint here File "/venv2/lib/python3.8/site-packages/rest_framework/relations.py", line 393 and inspect what self is, else you're not going to solve this issue.Vicenta
Melvyn: That's my question, too. It's set properly in BasicTeacherSerializer. As for the debugging, self.view_name='teacher_detail'. Its parent is ManyRelatedField(allow_empty=False, child_relation=HyperlinkedRelatedField(allow_empty=False, read_only=True, view_name='teacher-detail'), read_only=True)Madrigal
...and self is HyperlinkedRelatedField(allow_empty=False, read_only=True, view_name='teacher-detail')Madrigal
The fact that there's no queryset showing in that leads me to believe that it's actually the teacher field, not the teachers_id field, that we're hitting here. That doesn't make sense, since it's a write, but I had to add the read_only_fields bit in the meta after I was getting 'teacher' is required inexplicably on POSTsMadrigal
V
1

So, what I missed is that these are HyperlinkedModelSerializer. The difference with Assessment is how you handle the ManyToMany.

A HyperlinkedModelSerializer, generates HyperlinkedRelatedField for related fields and generates the view_name from rest_framework.utils.get_detail_view_name, which doesn't have a facility for an app name.

This is done by build_field, which delegates to build_relational_field based on model info obtained from rest_framework.utils.model_meta.get_field_info().

Your teacher field is probably in there.

I'm not certain why it works for Assessment as I can't find the condition that would reject/accept either, but my gut says that because PrimaryKeyRelatedField is a related field, it doesn't build a HyperlinkedRelatedField.

Either way, you should see what field name(s) is/are passed to build_relational_field to figure this out.

Solution

Remove the trailing comma after the field definition of teacher:

teacher = serializers.PrimaryKeyRelatedField(many=True, read_only=True),
                                                                 ------^

This turns teacher into a tuple and teachers_id disappears as well. As a result, the standard hyperlinked related field is created:

SectionInfoSerializer(instance=<Section: Section object (1)>):
    url = HyperlinkedIdentityField(view_name='gbook:section-detail')
    teachers_id = PrimaryKeyRelatedField(allow_empty=False, many=True, queryset=<QuerySet [<Teacher: Teacher object (1)>]>, source='teacher', write_only=True)
    send_emails = BooleanField(required=False)
    teacher = HyperlinkedRelatedField(allow_empty=False, many=True, read_only=True, view_name='teacher-detail')
    course = HyperlinkedRelatedField(allow_empty=False, many=True, read_only=True, view_name='course-detail')
Vicenta answered 24/12, 2020 at 17:32 Comment(5)
Course does not have a teacher. None of the related models have any related objects containing Teacher, and teacher-detail (without the appname before it) is not present anywhere in the projectMadrigal
Updated. Hopefully this gets you there, but without stepping through it, it's hard to see where the real culprit is.Vicenta
Thanks for the leads - I'm tracing that through. It's worth noting that if teacher is a PrimaryKeyRelatedField, this still doesn't work.Madrigal
Looks like build_relational_field is not called for this field. I get a hit in the debugger when it processes course_id (another field built exactly the same way), but not for standards_id in the correctly-working assessment serializerMadrigal
That's a nifty catch! I was also able to solve it, with commas still in, by modifying the field in the model: teachers = models.ManyToManyField(Teacher, db_table='gbook_section_teacher') ...but the commas were indeed an easy way to solve it. Thanks!Madrigal

© 2022 - 2024 — McMap. All rights reserved.