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


この記事は「さくらインターネット 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 の指定があった場合はそちらを優先するようになりました。めでたしめでたしです。