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
DeltaG
  • 760
  • 2
  • 9
  • 28
  • 1
    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](https://stackoverflow.com/a/8844834/8283848), and use a **`grep`** expression to see what is the actual URL name – JPG Dec 24 '20 at 16:29
  • 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 of – DeltaG Dec 24 '20 at 16:33
  • There is a `teachers_id` (plural), but a `teacher` field (singular). Copy/paste error or real issue? –  Dec 24 '20 at 16:34
  • You can use a [**`HyperlinkedRelatedField`**](https://www.django-rest-framework.org/api-guide/relations/#hyperlinkedrelatedfield) with **`view_name`** parameter @DeltaG – JPG Dec 24 '20 at 16:38
  • @JPG 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. –  Dec 24 '20 at 16:44
  • @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 here – DeltaG Dec 24 '20 at 16:47
  • 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. –  Dec 24 '20 at 16:50
  • 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. –  Dec 24 '20 at 16:55
  • 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)` – DeltaG Dec 24 '20 at 17:03
  • ...and `self` is `HyperlinkedRelatedField(allow_empty=False, read_only=True, view_name='teacher-detail')` – DeltaG Dec 24 '20 at 17:04
  • 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 POSTs – DeltaG Dec 24 '20 at 17:10

1 Answers1

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')
  • `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 project – DeltaG Dec 24 '20 at 18:12
  • Updated. Hopefully this gets you there, but without stepping through it, it's hard to see where the real culprit is. –  Dec 24 '20 at 19:22
  • 1
    Thanks for the leads - I'm tracing that through. It's worth noting that if `teacher` is a `PrimaryKeyRelatedField`, this still doesn't work. – DeltaG Dec 24 '20 at 19:46
  • 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 serializer – DeltaG Dec 24 '20 at 20:27
  • 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! – DeltaG Dec 24 '20 at 22:22