この記事は「さくらインターネット 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 |
モデルとシリアライザ
使うモデルとシリアライザです。モデルには記事のタイトル、本文、作成日時が定義されています。
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('記事の作成日時')
from rest_framework import serializers
from .models import Article
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ('title', 'body', 'created_at',)
ビューの実装
これからメインとなるビューの実装を説明していきます。まずは基本となる記事一覧を取得するビューからです。
記事一覧を取得する
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週間前以降に作成された記事を返すようにします。
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
を追加して次のようになります。
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クエリパラメータがある場合とない場合で処理を分けるようにします。
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
の指定があった場合はそちらを優先するようになりました。めでたしめでたしです。