So I'm trying to put together a not-so-simple Todo application, using some perhaps superfluous practices and technologies to showcase development skills. The application is meant to be a sort of Trello replica. The backend is Django utilizing the django REST framework for RESTful api requests from the frontend which is React.js. In short, I'm having trouble with my django_backend, and maybe specifically django-rest-framework, which doesn't seem to appreciate being asked to filter its querysets. I've tried several django packages as well as the method described in the DRF documentation, which I will elaborate, but my attempts aren't working and I would appreciate some guidance. Thank you!
I've just completed implementing redux-saga and I have a few very basic sagas that get EVERY instance of each of the models. But I obviously don't want to request every instance, so we should filter the response.
Django REST framework uses built in classes like Serializers and ViewSets to return lists of model instances that the rest-framework can respond to a request with. These were my initial viewsets for testing requests to the backend, and they all returned the appropriate JSON object to my frontend: viewsets.py
from rest_framework import viewsets
from corkboard.models import Card, Stage, Board
from .serializers import CardSerializer, StageSerializer, BoardSerializer
# Card is the model for a "Todo" Instance
# Todo Cards viewset
class CardViewSet(viewsets.ModelViewSet):
queryset = Card.objects.all()
serializer_class = CardSerializer
# Stages of completion model viewsets
class StageViewSet(viewsets.ModelViewSet):
queryset = Stage.objects.all()
serializer_class = StageSerializer
# Board that "houses" all stages and cards
class BoardViewSet(viewsets.ModelViewSet):
queryset = Board.objects.all()
serializer_class = BoardSerializer
viewseturls.py:
from rest_framework import routers
from .views import CardViewSet, StageViewSet, BoardViewSet
# registering router paths for each of the viewsets
router = routers.DefaultRouter()
router.register('cards', CardViewSet, 'cards')
router.register('stages', StageViewSet, 'stages')
router.register('boards', BoardViewSet, 'boards')
urlpatterns = router.urls
and these urls are used in the calls from the frontend via axios. Initially I tried simply changing the url in the request from something like
axios.get(`http://localhost:8000/api/cards)
to
axios.get(`http://localhost:800/api/cards/?stage=1`)
This different url responded with the same JSON object (ALL Card instances, regardless of stage)
Looking into the DRF filtering docs revealed some filtering methods that seem helpful. They involve creating another view class to override the get_queryset method as such:
from rest-framework import generics
class CardsFilteredByStageViewSet(generics.ListAPIView):
serializer_class = TodoSerializer
def get_queryset(self):
"""
This view should return a list of all the todo cards for
the stage as determined by the stage portion of the URL.
"""
stage = self.kwargs['stage']
return Card.objects.filter(stage=stage)
Note that this involves using the
rest-framework.generics.ListAPIView
and that I also tried inheriting from
views.ViewSets
After both, I go edit the router url in urls.py
:
from .views import StageCardViewSet
router.register('stage_cards', StageCardViewSet, 'stage_cards')
and then try
http://localhost:8000/api/stage_cards/
and
http://localhost:8000/api/stage_cards/?stage=1
but got a KeyError for both:
Request Method: GET
Request URL: http://localhost:8000/api/stage_cards/?stage=1
Django Version: 3.0.5
Exception Type: KeyError
Exception Value: 'stage'
The DRF docs also provide sources for packages like django-filter, and installation seems to go fine. I used
pipenv install django-filter
As you can see later on in this post, this package appears in my requirements.txt as well as my Pipfile, so it is certainly installed. It's also been added to django_backend.settings. Yes, the 's' is supposed to be in 'django-filters' below:
INSTALLED_APPS = [
# confusing pluralization
'django-filters',
...
]
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
],
...
}
This should give an error if it's not added as a package, correct?
After it's added as an installed app develop the existing viewsets (or generic lists, depending on what you're using)
from django_filters.rest_framework import DjangoFilterBackend #added
class CardViewSet(viewsets.ModelViewSet):
queryset = Card.objects.all()
serializer_class = CardSerializer
filter_backends = [DjangoFilterBackend] #added
filterset_fields = ['stage', 'id'] #added
But I get an "Unable to import" Error in the IDE. Exploring this error revealed this article which suggested addition of django-rest-framework-filters, but immediately after installation, I began getting errors about not having django.utils.six, which I found has been removed from django > 3, but is required for django-rest-framework-filters. Though some other documentation mentions a lot of d-r-r-f features being added back into the django-filter. Removing/uninstalling d-r-f-f fixes the errors and allows the server to run, but I'm still getting red underlines about being unable to import django-filter to my viewsets:
Unable to import 'django_filters.rest_framework'pylint(import-error)
in the updated views:
from django_filters.rest_framework import DjangoFilterBackend
#^^^ here is the unable to import error underline
I just want to filter the instances of my models so I can return relevant information to the frontend. Please help.
Though my project also contains a Pipfile for my local VDE, it also has a requirements.txt file used in the installation of dependencies for Docker builds. I'm including both.
requirements.txt:
appdirs==1.4.3
asgiref==3.2.7
astroid==2.3.3
certifi==2019.11.28
distlib==0.3.0
Django==3.0.5
django-environ==0.4.5
django-filter==2.2.0
djangorestframework==3.11.0
filelock==3.0.12
isort==4.3.21
lazy-object-proxy==1.4.3
mccabe==0.6.1
pipenv==2018.11.26
pylint==2.4.4
pylint-django==2.0.15
pylint-plugin-utils==0.6
pytz==2019.3
six==1.14.0
sqlparse==0.3.1
virtualenv==20.0.2
virtualenv-clone==0.5.3
wrapt==1.11.2
Pipfile:
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
[packages]
appdirs = "==1.4.3"
asgiref = "==3.2.7"
astroid = "==2.3.3"
certifi = "==2019.11.28"
distlib = "==0.3.0"
djangorestframework = "==3.11.0"
filelock = "==3.0.12"
isort = "==4.3.21"
lazy-object-proxy = "==1.4.3"
mccabe = "==0.6.1"
pipenv = "==2018.11.26"
pylint = "==2.4.4"
pytz = "==2019.3"
six = "==1.14.0"
sqlparse = "==0.3.1"
virtualenv = "==20.0.2"
virtualenv-clone = "==0.5.3"
wrapt = "==1.11.2"
Django = "==3.0.5"
django-environ = "*"
django-filter = "*"
[requires]
python_version = "3.8"
The Dockerfile for django_backend:
# Use an official Python runtime as a parent image
FROM python:latest
# Adding backend directory to make absolute filepaths consistent across services
WORKDIR /usr/src/app/django-backend
# Install Python dependencies
ADD requirements.txt .
RUN pip3 install --upgrade pip -r requirements.txt
# Add the rest of the code
ADD . .
# Make port 8000 available for the app
EXPOSE 8000
# Be sure to use 0.0.0.0 for the host within the Docker container,
# otherwise the browser won't be able to find it
CMD python3 manage.py runserver 0.0.0.0:8000
The frontend is React.js with redux and redux-saga. I don't believe it's relevant to this post as everything was working well until I started changing code in the django_backend
Of course ask any questions if I wasn't clear enough, and thanks in advance.
My first post ended up being because of a silly mistake. Answering it myself, because why not.
The pylint error is a big flag. This may just be an IDE configuration issue, and if I'm able to host locally with python manage.py runserver
without any errors, then it's possible you implemented it correctly. If all I'm getting is a pylint error then try running it anyway. As you said it ran with no errors right after removing django-rest-framework-filters.
The code sections I gave seemed like the correct implementation of django-filter as the DRF docs do a decent job of explaining how to use it for basic filtering. They also recommend another package called django-rest-framework-filters as mentioned, but I understand there are issues with django.utils.six
being removed from Django >3. One of that thread's comments includes a fix via installing the six
package that allegedly works with Django 3.0.4, so I would explore that if instance interrelationships are important for filtering.
Indeed, I had implemented it correctly. My guess is that I missed it because of some other Error overlap along the process, and the pylint error threw me off the scent. I simply missed that it was in fact running; I just didn't test its functionality. I also wanted to start using Stackoverflow as a resource for development, but going through the process brought me to the answer. Funny.
Reiterating correct implementation procedure:
pipenv install django-filter
Add to django_backend.settings:
INSTALLED_APPS = [
# confusing pluralization
'django-filters',
...
]
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
],
...
}
and refactor of viewsets:
from django_filters.rest_framework import DjangoFilterBackend # added
# Todo Card viewset now filterable via filterset_fields
class CardViewSet(viewsets.ModelViewSet):
queryset = Card.objects.all()
serializer_class = CardSerializer
filter_backends = [DjangoFilterBackend] # added
filterset_fields = ['stage', 'title', 'id'] # added
Now after python manage.py runserver
go to the right url (http://localhost:8000/api/cards/?stage=1) and you should see only cards in stage 1. This same url structures will hopefully also be callable from the frontend. But the django rest framework UI returns a filtered list. Success.
The pylint error was easily solved by going to the settings.json
file of my IDE (which happens to be VSCode, though I'm considering alternatives) and adjusting as such:
{
"python.PythonPath": "/user/local/bin", // added
...
}
The import error disappeared.