웹프로그래밍/Django

[Django] widget (3) widget 만들어보기 - 자동완성 Select2

ssung.k 2019. 8. 23. 00:15

 

 

widget (2)

 

[Django] widget (2) widget 만들어보기 - 별점 주기 rateit.js

widget (1) [Django] widget (1) widget의 원리와 widget 만들어보기 - 실시간 글자수 표시 widget input 태그는 type 속성에 따라 여러 모습을 보여줍니다. text 일 때는 글을 입력할 수 있도록, date 일 때는 날..

ssungkang.tistory.com

개별적인 내용을 담고 있지만 이전 포스팅과 전체적인 맥락은 이어지므로 참고하시면 도움이 되실 겁니다.

현재 모델은 다음과 같습니다.

# 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

내부 클래스 MediajQueryselect2 에 대한 파일들을 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 로 처리해주었습니다.

 

ajax 를 이용한 자동완성 구현