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
를 사용하여 쿼리할 수 있다.
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으로 막아준 것이다.
다들 알다시피 장고는 모델에서 명시한 필드에 맞춰 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는 Admin Class안에 명시하여 사용한다.
필드별로 일일이 위젯 설정하기 귀찮은 개발자들을 위해 만들어진 것으로 특정 타입을 가진 필드들에 특정 위젯을 일괄 적용하고 싶을 때 사용한다.
자세한 설명은 document를 참고하자
아래 코드로 예를 들면 JsonField들은 모두 JSONEditorWidget을 가진다는 뜻이다.
formfield_overrides = {
models.JSONField: {'widget': JSONEditorWidget},
}
어차피 같은 필드 쓰면 다 위젯 지정할 거 나는 호기롭게 DateTimeRangeField에도 formfield_overrides를 사용해보았지만
그 어떤 위젯도 나타나지 않았다...
이유가 뭘까? 그것은 구글링해도 알 수 없는 사실이었기에 formfield_overrides 코드를 뜯어볼 수밖에 없었다. 그리고 디폴트에 전부 django models에서 나온 필드들만 dict의 key로 지정되어있는 것을 볼 수 있었다.
ㅇ ㅏ...이것도 django.db가 제공해주는 field가 아니라서 그런가부다 했다..
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 처리를 자동으로 해준다니 더더욱 유용할 듯.
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 두 개 쓸 때가 더 행복했다.