django-rest-framework

Django REST framework のURLクエリパラメータ実装でつまづいた話

More than 1 year has passed since last update.

この記事は「さくらインターネット Advent Calendar 2017」の10日目の記事です。

以前 Django REST framework でURLクエリパラメータを実装していてつまづいたことがあったのでその話をしようと思います。


概要

例えば記事一覧を取得するAPIがあったとして、デフォルトは1週間前以降に作成された記事一覧を返しています。このAPIでURLクエリパラメータを指定して2週間前以降に作成された記事一覧を取得しようとしましたがうまくいきません。2週間前を指定しても1週間前以降に作成された記事が返ってきてしまっていました。

環境は以下のとおりです。

パッケージ
バージョン

Django
1.11.7

django-filter
1.1.0

djangorestframework
3.7.3


モデルとシリアライザ

使うモデルとシリアライザです。モデルには記事のタイトル、本文、作成日時が定義されています。


models.py

from django.db import models

class Article(models.Model):

class Meta:
db_table = 'articles'

title = models.CharField('記事のタイトル', max_length=100)
body = models.TextField('記事の本文')
created_at = models.DateTimeField('記事の作成日時')



serializers.py

from rest_framework import serializers

from .models import Article

class ArticleSerializer(serializers.ModelSerializer):

class Meta:
model = Article
fields = ('title', 'body', 'created_at',)



ビューの実装

これからメインとなるビューの実装を説明していきます。まずは基本となる記事一覧を取得するビューからです。


記事一覧を取得する


views.py

from rest_framework import viewsets, mixins

from .models import Article
from .serializers import ArticleSerializer

class ArticleViewSet(mixins.ListModelMixin,
viewsets.GenericViewSet):

queryset = Article.objects.all()
serializer_class = ArticleSerializer


これで記事一覧を取得できるようになりました。


1週間前以降に作成された記事一覧を取得する

さきほどのビューを修正してデフォルトで1週間前以降に作成された記事を返すようにします。


views.py

import isodate

from datetime import datetime

from rest_framework import viewsets, mixins

from .models import Article
from .serializers import ArticleSerializer

class ArticleViewSet(mixins.ListModelMixin,
viewsets.GenericViewSet):

queryset = Article.objects.none()
serializer_class = ArticleSerializer

def get_queryset(self):
offset = isodate.parse_duration('P1W')
date = datetime.now() - offset
return Article.objects.filter(created_at__gte=date)


get_queryset を定義してクエリセットにフィルタを付けています。これで1週間前以降に作成された記事を返すようになりました。


URLクエリパラメータで作成日時を絞り込む(失敗!)

続いてURLクエリパラメータ created_after を付けて1日前や2週間前といった絞り込みが行えるようにします。 django_filters を追加して次のようになります。


views.py

import isodate

from datetime import datetime

import django_filters as filters

from rest_framework import viewsets, mixins

from .models import Article
from .serializers import ArticleSerializer

class ArticleFilter(filters.FilterSet):

created_after = filters.CharFilter(method='filter_created_after')

class Meta:
model = Article

def filter_created_after(self, qs, name, value):
try:
offset = isodate.parse_duration(value)
except:
offset = isodate.parse_duration('P1W')

date = datetime.now() - offset
return qs.filter(created_at__gte=date)

class ArticleViewSet(mixins.ListModelMixin,
viewsets.GenericViewSet):

queryset = Article.objects.none()
serializer_class = ArticleSerializer
filter_backends = (filters.rest_framework.DjangoFilterBackend,)
filter_class = ArticleFilter

def get_queryset(self):
offset = isodate.parse_duration('P1W')
date = datetime.now() - offset
return Article.objects.filter(created_at__gte=date)


しかしこれではURLクエリパラメータを指定しても有効になりません。 get_queryset で先に1週間前のフィルタを付けているのでURLクエリパラメータの指定が無視されてしまっていました。


URLクエリパラメータで作成日時を絞り込む(成功!!)

そこで少し書き換えてURLクエリパラメータがある場合とない場合で処理を分けるようにします。


views.py

import isodate

from datetime import datetime

import django_filters as filters

from rest_framework import viewsets, mixins

from .models import Article
from .serializers import ArticleSerializer

class ArticleFilter(filters.FilterSet):

created_after = filters.CharFilter(method='filter_created_after')

class Meta:
model = Article

def filter_created_after(self, qs, name, value):
try:
offset = isodate.parse_duration(value)
except:
offset = isodate.parse_duration('P1W')

date = datetime.now() - offset
return qs.filter(created_at__gte=date)

class ArticleViewSet(mixins.ListModelMixin,
viewsets.GenericViewSet):

queryset = Article.objects.none()
serializer_class = ArticleSerializer
filter_backends = (filters.rest_framework.DjangoFilterBackend,)
filter_class = ArticleFilter

def get_queryset(self):
qs = Article.objects.all()

'''
URLクエリパラメータに created_after の指定があった場合は
queryset に created_at__gte フィルタを付けない。
'''

if 'created_after' in self.request.query_params:
return qs

offset = isodate.parse_duration('P1W')
date = datetime.now() - offset
return qs.filter(created_at__gte=date)


これでURLクエリパラメータに created_after の指定があった場合はそちらを優先するようになりました。めでたしめでたしです。