Django에 DateTimeRangeField 적용하기

haremeat·2022년 8월 4일
0

Django

목록 보기
10/16
post-thumbnail
post-custom-banner

Django에서 날짜/시간을 지정해야 할 때 보통 DateTimeField를 많이 쓸 것이다. 하지만 시작일과 종료일을 함께 지정해야 하는 경우 DateTimeRangeField를 써 보는 것도 또 다른 방법 중 하나다.
DateTimeRangeField는 이름 그대로 날짜/시간의 범위를 저장하는 필드이다.

보통 django.db의 models에서 바로 뽑아오는 다른 장고 필드들과 달리 PostgreSQL 전용 필드기때문에 아래와 같이 postgres에서 불러와서 써야한다.

from django.contrib.postgres.fields import DateTimeRangeField

class MyModel(models.Model):
	datetime_example = DateTimeRangeField(_('예시 범위'))

쿼리

범위가 저장되므로 시작일과 종료일을 어떤 식으로 쿼리할지가 가장 궁금할 것이다. (나는 그랬다.)

now = timezone.now()
MyModel.objects.filter(datetime_example__startswith__lt=now)
MyModel.objects.filter(datetime_example__endswith__gt=now)
MyModel.objects.filter(datetime_example__contains=now)

시작일은 필드명 뒤에 __startswith__를 붙이면 되고, 종료일은 __endswith__를 붙이면 된다. 현재 시간이 범위 안에 포함되게 하고 싶은 경우 위와 같이 __contains를 사용하여 쿼리할 수 있다.

clean 처리

def clean(self):
    if not self.datetime_example or (not all([self.datetime_example.lower, self.datetime_example.upper])):
        raise ValidationError('시작일과 종료일을 지정해주세요')

    if self.datetime_example.lower >= self.datetime_example.upper:
        raise ValidationError(_('시작일이 종료일보다 같거나 클 수 없습니다.'))

쿼리와 달리 clean에서 처리할 때는 시작일은 .lower 종료일은 .upper로 가져온다.

환장하는 포인트 : 필수값임에도 자동으로 validation 처리를 해 주지 않고 그저 오류를 내뱉는 바람에 저렇게 일일이 날짜 입력하도록 처리해줬음.

아 그리고 사실 아래의 self.datetime_example.lower >= self.datetime_example.upper 이 부분 처리는 무조건 할 필요는 없다. 음, 좀 더 정확히 말하자면 부등호는 필요없고 (범위필드니까 기본적으로 못하게 막아준다.) equal(=)표시가 필요해서 넣은 것이다. 시간,분,초까지 모두 동일한 시작일과 종료일을 지정할 경우 저장이... 안 되어야 하는데... 저장되면서 해당 필드에 빈 값이 들어가는 문제가 발생한다. 그래서 따로 clean으로 막아준 것이다.

Django Admin에서 widget 자동으로 안 만들어줌

다들 알다시피 장고는 모델에서 명시한 필드에 맞춰 Admin에서 알아서 input 타입을 지정해준다.
아래처럼 DateTimeField필드를 사용하면 알아서 날짜 위젯이 뜨는 식이다.

킹치만 DateTimeRangeField는 그딴 거 없다. 어드민에 들어가면 덩그러니 일반 input창이 우릴 반겨준다. 장고가 제공하는 필드가 아니라서 그런 듯 하다.
그래서 따로 admin.py에서 form처리를 해 줘야 한다.

class MyModelAdminForm(forms.ModelForm):
    class Meta:
        model = MyModel
        fields = ['id',...]
        widgets = {
            'datetime_example': RangeWidget(
                forms.DateTimeInput(attrs={'type': 'datetime-local'})
            )
        }


@admin.register(MyModel)
class MyModelAdmin(SortableAdminMixin, admin.ModelAdmin):
    list_display = ['id', ...]
    form = MyModelAdminForm

나는 위 코드처럼 따로 MyModelAdminForm을 만들어줬다.

formfield_overrides 사용 못해

근데 만들고보니 formfield_overrides를 사용할 수 있지 않을까?
라는 생각이 들었다.
formfield_overrides는 Admin Class안에 명시하여 사용한다.
필드별로 일일이 위젯 설정하기 귀찮은 개발자들을 위해 만들어진 것으로 특정 타입을 가진 필드들에 특정 위젯을 일괄 적용하고 싶을 때 사용한다.
자세한 설명은 document를 참고하자

아래 코드로 예를 들면 JsonField들은 모두 JSONEditorWidget을 가진다는 뜻이다.

formfield_overrides = {
	models.JSONField: {'widget': JSONEditorWidget},
}

어차피 같은 필드 쓰면 다 위젯 지정할 거 나는 호기롭게 DateTimeRangeField에도 formfield_overrides를 사용해보았지만
그 어떤 위젯도 나타나지 않았다...
이유가 뭘까? 그것은 구글링해도 알 수 없는 사실이었기에 formfield_overrides 코드를 뜯어볼 수밖에 없었다. 그리고 디폴트에 전부 django models에서 나온 필드들만 dict의 key로 지정되어있는 것을 볼 수 있었다.
ㅇ ㅏ...이것도 django.db가 제공해주는 field가 아니라서 그런가부다 했다..

RangeOperator를 이용한 constraints

class Meta:
    ordering = ['created']
    constraints = [
        ExclusionConstraint(
            name='exclude_overlapping_my_fields',
            expressions=[
                ('datetime_example', RangeOperators.OVERLAPS),
                ('user', RangeOperators.EQUAL),
            ],
        )
    ]

불편한 점만 이야기했지만 편한 점도 있다. RangeOperators를 이용하여 constraints를 간단하게 걸 수 있다.

위 코드를 해석해보면 datetime_exmaple은 같은 user에 한해 절대 중복을 허용하지 않는다는 뜻이다.
딱 봐도 예약 등을 처리할 때 자주 쓰일 제약 조건이다.

특히나 Django 4.1부터는 constraints를 통해서 model validation 처리를 자동으로 해준다니 더더욱 유용할 듯.

RangeBoundary

API 만들어서 호출해보면 DateTimeRangeField를 사용한 필드는 아래와 같은 형식으로 출력되는 걸 볼 수 있다.

"datetime_example": {
    "lower": "2022-08-03 10:14:00",
    "upper": "2022-08-03 14:11:00",
    "bounds": "[)"
}

여기서 저 bounds의 괄호들이 뭔지 궁금했는데 document 가니까 나와있었다.

RangeBoundary(inclusive_lower=True, inclusive_upper=False)

기본적으로 RangeBoundary class는 inclusive_lower는 true이고 inclusive_upper는 false인데 이는 시작일은 포함하고 종료일 포함하지 않는다는 뜻이다.
한 마디로 범위를 뜻하는 거다.
대괄호 []는 포함, 소괄호 ()는 미포함을 의미한다.

결론

개인적으로는 쓰기 좀 귀찮은 필드인 것 같다.
그냥 DatetimeField 두 개 쓸 때가 더 행복했다.

profile
버그와 함께하는 삶
post-custom-banner

0개의 댓글