Django 를 사용하면 filter 관련 패키지는 대부분 django-filter 라는 패키지를 사용한다.
모든 사용자들이 그러겠지만 어떠한 패키지를 사용할 때 패키지가 주는 좋은 점도 있지만 반대로 패키지가 제약을 걸어서 불편한 점이 존재한다
저에게 있어서 django-filter 가 주는 불편한 점은 multiple filter 관련의 choices 와 queryset 과 모든 필드를 커버 못하는 multiple filter 였다.
# 기본적인 코드
class UserFilter(django_filters.FilterSet):
username = filters.CharFilter()
login_timestamp = filters.IsoDateTimeFilter(field_name='last_login')
# multiple 필터를 구현하기 위하 조건들
# (field_name, to_field_name, queryset(choices) 이 필요
class FooFilter(BaseFilterSet):
foo = django_filters.filters.ModelMultipleChoiceFilter(
field_name='attr__uuid',
to_field_name='uuid',
queryset=Foo.objects.all(),
)
여기에서 불편한 점이 존재한다.
그래서 해당하는 불편함을 개선하기 위해서 커스텀해서 사용했었는데 코드는 아래와 같다.
from django.utils.translation import gettext as _
from django_filters import rest_framework as filters
from django import forms
from django.core.exceptions import ValidationError
class MultipleField(forms.Field):
widget = forms.MultipleHiddenInput
default_error_messages = {
'invalid': _('Enter a valid data.'),
}
def __init__(self, coerce=None, *args, **kwargs):
self.coerce = coerce
super().__init__(*args, **kwargs)
def validate(self, value):
if self.coerce and isinstance(value, list):
try:
value = [self.coerce(v) for v in value]
except ValueError:
raise ValidationError(self.error_messages['invalid'], code='invalid')
return value
class MultipleFilter(filters.MultipleChoiceFilter):
field_class = MultipleField
코드는 어떻게 보면 간단하다.
MultipleChoiceFilter 를 상속받고 필드 클래스를 커스텀 한 MultipleField 로 설정해주면 끝이다.
왜 이렇게 구현할 수 있는지는 기존 MultipleChoiceFilter 를 보면 편하다.
class MultipleChoiceFilter(Filter):
field_class = MultipleChoiceField
# 여기에서 필드 클래스를 더 타고 들어가면
class MultipleChoiceField(ChoiceIteratorMixin, forms.MultipleChoiceField):
iterator = ChoiceIterator
# 이렇게 나오는데 그러면 MultipleChoiceField 를 들어가면 어떻게 동작하는지를 알 수 있다.
class MultipleChoiceField(ChoiceField):
hidden_widget = MultipleHiddenInput
widget = SelectMultiple
default_error_messages = {
"invalid_choice": _(
"Select a valid choice. %(value)s is not one of the available choices."
),
"invalid_list": _("Enter a list of values."),
}
# 기본적으로 hidden_widget 에서 multiple 에 관련된 것을 제공하고 제약적인 부분은 choicefield를 통해서 상속받는다.
# 이것이 확증이 되려면 ChoiceField 가 원하는 동작을 하는지 확인하면 된다.
class ChoiceField(Field):
widget = Select
default_error_messages = {
"invalid_choice": _(
"Select a valid choice. %(value)s is not one of the available choices."
),
}
def __init__(self, *, choices=(), **kwargs):
super().__init__(**kwargs)
self.choices = choices
# 여기서 init 을 보면 choices 를 받는 것이 보이고 invalid 한 경우 해당하는 에러를 띄우는 것을 볼 수 있다.
# 그렇다면 여기에서 필드만 다른 것을 상속받으면 된다는 것을 알 수 있고 해당 부분의 경우 나는 forms.Field 제일 기본적인 부분을 상속받았다.
그래서 결국에는 어떻게 코드가 개선되었는 지를 보면
class StateFilter(filters.FilterSet):
big_cities = MultipleFilter(field_name='big_cities__name', coerce=str)
small_cities = MultipleFilter(field_name='big_cities__small_cities__name', coerce=str)
이렇게 변경이 되어졌다.
내가 본 장점은 이렇다.
그리고 반대로 단점도 존재하는데
1. 필드에 대한 제약조건이 없음으로 인해서 validation check 가 없어짐
나는 해당부분을 간단하게 해결을 하기위해서 coerce를 제공하는 방향으로 구현을 했다.
좀 더 다양하게 구현하고 싶은 경우에는 내가 생각했을 때 두 가지 방법이 있다.
이렇게 하면 좀더 의미에 맞는 multiple filter들을 만들 수 있을 것이다.
만약에 간단하게 경험하고 싶으면django-custom-filter 를 보면 될 것 같다.