[Django] widget (3) widget 만들어보기 - 자동완성 Select2
개별적인 내용을 담고 있지만 이전 포스팅과 전체적인 맥락은 이어지므로 참고하시면 도움이 되실 겁니다.
현재 모델은 다음과 같습니다.
# core/models.py
from django.db import models
from django.core.validators import MinValueValidator, MaxValueValidator
# 생략
class Student(models.Model):
university = models.ForeignKey(University, on_delete=models.CASCADE)
date_birth = models.DateField()
residencce = models.CharField(max_length=200)
photo = models.ImageField(blank=True)
grade = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(5)])
intro = models.TextField()
이번에는 Student 모델에 대해 본인의 대학을 선택할 때, 기존에 등록된 대학에 대해 자동완성을 할 수 있도록 구현해보도록 하겠습니다. 이 역시 Select2
모듈을 사용해도록 하겠습니다.
Select2 를 이용한 기본적인 자동완성 구현
# core/forms.py
from django import forms
from .models import University, Student
from .widgets import CounterTextInput, starWidget, AutoCompleteWidget
# 생략
class StudentForm(forms.ModelForm):
class Meta:
meodel = Student
fields = '__all__'
widgets = {
'university': AutoCompleteWidget,
'grade': starWidget,
}
저번 시간에 만들었던 starWidget
에 이어서 AutoCompleteWidget
이라는 커스텀 widget 을 정의하여 사용해보겠습니다.
<!-- templates/widgets/autocomplete_select.html -->
{% include "django/forms/widgets/select.html" %}
{{ form.media }}
<script>
$(function(){
$('#{{ widget.attrs.id }}').select2();
})
</script>
select.html
을 include 하여 이에 맞게 select input 을 생성하고 js 에서 이 input에 대해서 select2
메소드를 호출하면 자동완성을 지원해줍니다.
# core/widgets.py
class AutoCompleteWidget(forms.Select):
template_name = 'widgets/autocomplete_select.html'
class Media:
css = {
'all' : [
"//cdnjs.cloudflare.com/ajax/libs/select2/4.0.8/css/select2.min.css",
],
}
js = [
"//code.jquery.com/jquery-3.4.1.min.js",
"//cdnjs.cloudflare.com/ajax/libs/select2/4.0.8/js/select2.min.js",
]
def build_attrs(self, *args, **kwargs):
context = super().build_attrs(*args, **kwargs)
context['style'] = 'min-width:200px;'
return context
내부 클래스 Media
로 jQuery
와 select2
에 대한 파일들을 CDN 으로 가져옵니다. 또한 build_attrs
를 통해 입력 input의 styple 을 보기 좋게 수정해주었습니다.
그 결과 db 에 있는 학교 목록이 모두 불러와져 자동완성 목록에 뜨게 됩니다. 이렇게 매 순간 모든 학교 목록을 불러오는 것은 학교목록이 많아질수록 오버헤드로 작용합니다. 그래서 ajax 를 통해 이를 개선해보도록 하겠습니다.
ajax를 추가한 효율적인 자동완성 구현
# core/views.py
from django.shortcuts import render
from django.http import JsonResponse
from .models import University
def university_list(request):
queryset = University.objects.all()
query = request.GET.get('q')
queryset = queryset.filter(name__icontains=query)
results = [
{
'id': university.id,
'text': university.name,
} for university in queryset
]
data = {
'results': results,
}
return JsonResponse(
data,
json_dumps_params = {'ensure_ascii': False}
)
사용자가 input 태그에 값을 입력하면 이는 get 요청에 의해 querystring으로 함께 넘어오게 됩니다. 이 부분의 key 값이 q
이기 때문에 다음과 같이 입력한 부분이 들어가 있는 인스턴스만 가져와서 json으로 넘겨줍니다.
json_dumps_params 은 뭔가요?
JsonResponse 내부적으로 파이썬 객체를 json 으로 바꿔주기 위한 dump 가 실행됩니다. 이는 내부적으로
ensure_ascii
속성이 있고 default 값이 True 입니다. 이럴 경우, ascii 가 아닌 한글과 같은 경우 유니코드로 인코딩을 해줍니다. 따라서 이를 false 로 지정해주면 UTF8로 인코딩 되는 걸 확인할 수 있습니다.<!-- 유니코드로 인코딩 --> {"results": [{"id": 1, "text": "\uc11c\uc6b8\uc2dc\ub9bd\ub300"}, {"id": 2, "text": "\uacbd\ud76c\ub300"}, {"id": 3, "text": "\ud55c\uad6d\uc678\ub300"}, {"id": 4, "text": "\ub3d9\uad6d\ub300"}]} <!-- UTF로 인코딩 --> {"results": [{"id": 1, "text": "서울시립대"}, {"id": 2, "text": "경희대"}, {"id": 3, "text": "한국외대"}, {"id": 4, "text": "동국대"}]}
결과적으로는 어느걸로 인코딩이 되던 브라우저 단에서 js 객체로 잘 변환이 됩니다. 다만 제가 확인하기 편하기 위해서 설정해 주었습니다.
# core/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('university', views.university_list, name="university_list"),
]
이제 해당 url 로 접근 시에 json으로 데이터가 잘 넘어가는 걸 확인할 수 있었습니다. 이제 widget 이 해당 url 을 인식하고 값을 입력할 때마다 ajax 를 실행시켜보도록 하겠습니다.
# core/forms.py
from django import forms
from django.urls import reverse_lazy
from .models import University, Student
from .widgets import CounterTextInput, starWidget, AutoCompleteWidget
# 생략
class StudentForm(forms.ModelForm):
class Meta:
meodel = Student
fields = '__all__'
widgets = {
'university': AutoCompleteWidget(ajax_url=reverse_lazy('university_list')),
'grade': starWidget,
}
우선 AutoCompleteWidget
에게 어느 url 로 접근을 해야 ajax 를 실행 시킬 수 있을 시 넘겨줍니다. 이 떄 reverse
가 아닌 reverse_lazy
를 사용하였습니다. 둘은 결과적으로 같은 기능을 하지만 class 내에서 reverse
를 사용할 경우, resolvers.py 파일을 구문 분석하는 과정에서 reverse
가 실행 되므로 오류를 발생합니다. 그래서 실행 과정을 지연시키는 reverse_lazy
로서 이 문제를 해결 합니다.
또한 ajax_url
는 기존에 존재하는 필드가 아니라 지금 임의로 지정한 필드입니다. 그래서 오류가 발생하니 뒤에 widgets.py 에서 해당 필드를 추가하도록 하겠습니다.
# core/widgets.py
class AutoCompleteWidget(forms.Select):
template_name = 'widgets/autocomplete_select.html'
class Media:
css = {
'all' : [
"//cdnjs.cloudflare.com/ajax/libs/select2/4.0.8/css/select2.min.css",
],
}
js = [
"//code.jquery.com/jquery-3.4.1.min.js",
"//cdnjs.cloudflare.com/ajax/libs/select2/4.0.8/js/select2.min.js",
]
def build_attrs(self, *args, **kwargs):
context = super().build_attrs(*args, **kwargs)
context['style'] = 'min-width:200px;'
return context
def __init__(self, ajax_url, *args, **kwargs):
super().__init__(*args, **kwargs)
self.ajax_url = ajax_url
def get_context(self, *args, **kwargs):
context = super().get_context(*args, **kwargs)
context['ajax_url'] = self.ajax_url
return context
def optgroups(self, name, value, attrs=None):
existed_ids = [_id for _id in value if _id]
self.choices.queryset = self.choices.queryset.filter(id__in=existed_ids)
return super().optgroups(name, value, attrs=None)
3개의 함수가 추가되었습니다.
-
__init__
먼저 forms.py 에서
ajax_url
을 받아주기 위해서 init 함수를 재정의하였습니다.
-
get_context
templates 에서
ajax_url
을 사용하기 위해서는 get_context 메소드를 통해 넘겨줄 수 있습니다.
-
optgroups
모든 데이터에 대해서 queryset 을 가져왔었는데 optgroups 를 커스텀해서 입력하는 순간의 값들에 대해서만 가져오도록 하였습니다.
<!-- templates/widgets/autocomplete_select.html -->
{% include "django/forms/widgets/select.html" %}
{{ form.media }}
<script>
$(function(){
var options = { minimumInputLength: 1 };
var ajax_url = "{{ ajax_url |default:''}}";
if (ajax_url.length > 0){
options['ajax'] = {
'url': ajax_url,
'dataType': 'json',
};
}
$('#{{ widget.attrs.id }}').select2(options);
});
</script>
templates 에서 이에 대해 ajax 로 처리해주었습니다.