1
votes

How can Django REST Framework be used to render a list of model instances with specific fields editable by the user?

I'm a few months into Django, and only a couple days into DRF. I've tried several different approaches, but just can't seem to wrap my head around it.

Prior to using DRF, my workflow would have been to set up a view (and associated URL) that: queried my model, picked my custom form from forms.py (exposing only the fields I needed), put the two together into a dictionary, and sent it to a template.

In the template, I could then loop through my model instances and set up my editable fields, piping them through django crispy forms, as required.

I could then call this template through an AJAX get request.

models.py

class Buyer(models.Model):
  name = models.CharField(unique=True, max_length = 20)

class Item(models.Model):
  name = models.CharField(unique=True, max_length = 50)
  active = models.BooleanField(default=True)
  bought_by = models.ForeignKey(Buyer, null=True, blank=True, to_field="name",)

views.py

class ItemViewSet(viewsets.ModelViewSet):
  queryset = models.Item.objects.select_related("bought_by")
  serializer_class= serializers.ItemSerializer
  filterset_fields = ("bought_by")

serializers.py

class ItemSerializer(serializers.HyperlinkedModelSerializer):
  class Meta:
    model = models.Item
    fields = "__all__"
    extra_kwargs = {"url": {"view_name: "myapp:item-detail"}}

urls.py

router = routers.DefaultRouter()
router.register(r"items", views.ItemViewSet)

template.html

{% load static %}
{% load rest_framework %}

<table id="item_table" class="table">
  <thead>
    <tr>
      <th scope="col">Name</th>
      <th scope="col">Active</th>
      <th scope="col">Buyer</th>
    </tr>
  </thead>

  <tbody>
    {% for item in data %}
      <tr scope="row">
        <td>{{ item.name }}</td>
        <td>{{ item.active }}</td>
        <td>{{ item.bought_by }}</td>
      </tr>
    {% endfor %}
  </tbody>
</table>

Some Js file

function getData(){
  updateRequest = $.ajax({
    type: "GET",
    url: "myapp/items/",
    success: function(data) {
      //....
    }
  });
}

First Approach: Customize the ListModelMixin for ItemViewSet to render the template and pass along the serializer. Something along the lines of:

def list(self,request, *args, **kwargs):
  ...
  return Response ({'data': queryset, 'serializer': serializer}, template_name = "myapp/template.html")

then in template.html change {{ item.active }} to:

<form action="{{ item.url }}" method="post">
  {% csrf_token %}
  {{ render_form serializer }}
</form>

Error: serializer is not iterable. Make sense. Changed it to :

{{ render_field item.bought_by }}

Error: needs 'style', added that. continued to get other errors

Second Approach: Try to modify the ListModelMixin to collect a dictionary of serialized model instances e.g.,:

items= dict()
        for item in queryset:
            items[item] = serializers.ItemSerializer(item, data=request.data)

Never quite figured this out as serializers.ItemSerializer(item, data=request.data) doesn't seem to be a dictionary item and so can't use data.items() to iterate through it in the template.

Apologies for the long write up, but I'm a bit at sea and not quite sure how to proceed.

What is the most elegant DRF way of returning a list of all model instances with some fields editable (similar to how you would have in django admin with list_editable specified)?

I could always the older method, but it feels like I'm missing something obvious with DRF here.

References:

https://www.django-rest-framework.org/topics/html-and-forms/

Django Rest Framework serializers render form individually

Rendering a form with django rest framework's ModelViewSet class insead of APIView

1

1 Answers

1
votes

Solved

Approach two was closest: select a renderer conditionally and overwrite the default .list().

views.py:

from rest_framework import renderers
from rest_framework.response import Response

class ItemViewSet(viewsets.ModelViewSet):
  queryset = models.Item.objects.select_related("bought_by")
  serializer_class= serializers.ItemSerializer
  filterset_fields = ("bought_by")
  renderer_classes = [renderers.JSONRenderer, renderers.BrowsableAPIRenderer, renderers.TemplateHTMLRenderer]

  def list(self, request, *args, **kwargs):
    queryset = self.filter_queryset(self.get_queryset())

    if request.accepted_renderer.format == "html":
      items = list()

      for item in queryset:
        items.append({"serializer": self.get_serializer(ticket), "item": item})

      return Response(
        {
          "items_info": items,
          "style": {"template_pack": "rest_framework/inline/"},
        },
        template_name="myapp/items_list.html",
      )
    else:
     page = self.paginate_queryset(queryset)
     if page is not None:
       serializer = self.get_serializer(page, many=True)
       return self.get_paginated_response(serializer.data)

      serializer = self.get_serializer(queryset, many=True)

    return Response(serializer.data)

This will check if the requesting url has a /?format=html suffix. If so it will serialize each item in the queryset and include a list of {serializer:item} dictionaries in the dictionary (context) sent to items_list.html.

In order to render_field, DRF needs a style defined.

If the format suffix is not html, or is not specified, it will prioritize the JSON renderer or BrowsableAPI Renderer (the default renderers). This way, our browsable API and JSON api still work easily.

To render this with the bought_by field editable for each instance, modify your template (in this case items_list.html) along these lines:

{% load static %}
{% load rest_framework %}

{% if items_info %}
  {% csrf_token %}

  <table id="Items_Table" class="table">
    <thead>
      <tr>
       <th scope="col">Name</th>
       <th scope="col">Active</th>
       <th scope="col">Bought By</th>
      </tr>
    </thead>

    <tbody>
      {% for pair in items_info %}

        <tr scope="row">
          <td>{{ pair.item.name }}</td>
          <td>{{  pair.item.active  }}</td>
          <td>
            <form action="{% url "myapp:item-detail" pair.item.pk %}" method="PATCH">
            {% render_field pair.serializer.bought_by style=style %}
            </form>
          </td>
        </tr>

      {% endfor %}
    </tbody>
  </table>

{% else %}
  <p class="text-center">No items to show.</p>
{% endif %}

Now, in your GET request simply append "/?format=html" to the URL.

If you are using AJAX, when sending the POST/PUT/PATCH/etc. request, include the csrf token as described in the Django documentation.