장고 Summary - 8. Widgets-thumbnail

장고 Summary - 8. Widgets

자주 쓰이는 것들 위주로
356

Widget

장고에서 form 안의 input을 수정해주려고 할 때, forms.py를 상속받아 그대로 보여주고 있는 경우, input의 class나 다양한 attribute들을 컨트롤하기가 쉽지 않아진다. 보통 context에 forms.py에서 상속받은 form의 인스턴스를 그대로 넣어준 뒤 django template에서 뿌려주기 때문이다. 하지만, Client나 Design Team에서 그런 기본 form으로 만족할 리가 없다.
물론 django-bootstrap5django-crispy-forms, django-widget-tweaks 등을 이용해서 해결이 가능한 문제들도 많다. 심지어는 HTML, CSS, JS로도 어느 정도까지는 처리가 가능할 것 같다는 생각도 든다. 하지만 우선은 기본적으로 django에서 제공하는 widget의 활용법을 정리해두면 좋을 것이라는 생각에, 장고 마지막 정리 글쓰기의 주제로 widget을 정해보았다. CBV, 혹은 __str__과 같이 자주 활용되는 메서드들, test.py, 자주 범하는 실수들 같은 글들도 더 정리하고 싶기는 한데, 해당 내용들이 '아주 필수적인 스텝'에 들어갈지에 대한 고민이 좀 있어, 천천히 정리해도 되지 않을까 싶다. 물론 그렇다고 이미 정리해둔 Field Lookups가 과연 필수적인 스텝이냐고 묻는다면 할 말은 없다.

forms.Form을 상속받은 경우

class RegistrationForm(forms.Form):
    username = forms.CharField(max_length=20)
    password = forms.CharField(widget=forms.PasswordInput(
        attrs = {'placeholder': 'you should make password longer than 8 characters.'}
    ))

여기서 주의할 점은 forms에서 상속받은 Input의 이름들이 forms에서 상속받는 Field 이름들과는 조금씩 다르다는 것이다. 굳이 여기서 built-in widget들을 다 살펴볼 필요는 없을 것이다. 해당 내용은 여기를 참고하면 좋다.

forms.ModelForm 혹은 다른 built-in form(UserCreationForm, PasswordChangeForm, etc)을 상속받은 경우

1-1. forms.py에서 바로 수정하기
class BookForm(forms.ModelForm):

    class Meta:
        model = Book
        fields = [
            "title",
            "pud_year",
            "image",
            "review",
        ]
        widgets = {
            "review": forms.Textarea(
                attrs = {'placeholder': 'write your review as long as you can'}
            )
        }

forms.Form에서의 방식과 아주 유사하다.

1-2. forms.py에서 바로 수정하기 2
class CustomPasswordChangeForm(PasswordChangeForm):
    def __init__(self, *args, **kwargs):
        super(CustomPasswordChangeForm, self).__init__(*args, **kwargs)
        self.fields["old_password"].label = "OLD PASSWORD"
        self.fields["old_password"].widget.attrs.update(
            {
                "class": "text-muted",
                "autofocus": False,
            }
        )

첫째, 둘째 줄을 어쩌어찌 보아넘기다가 셋째 줄로 와서는 도저히 안 되겠다 싶어 뒤로가기를 누르려고 하시는 분들이 계실 것이다. 괜찮다, 차근차근 살펴보자.
첫째 줄에서 우리는 PasswordChangeForm을 상속받은 클래스 CustomPasswordChangeForm를 정의해주려고 한다.
두 번째 줄에서 우리는 __init__이라는 익숙하면서도 (제대로 알지 못하는 녀석이기에)무서워보이는 함수를 정의해줄 것이다. __init__()은 초기 환경 설정을 해주는 함수라고 생각하면 된다. 해당 클래스에서 사용할 변수 및 메서드들을 선언한다고 생각하면 좋다. 이 함수는 반드시 첫 번째 parameter로 self를 지정한다. self에는 인스턴스 자체가 들어있다. self를 선언해주어야 클래스는 자기자신이 가진 혹은 상속받은 변수 및 메서드에 접근할 수 있다.
세 번째 줄은 부모 및 조상 클래스로부터 변수 및 메서드들을 다 상속받아오는 과정이다. Python 3.x 버전 부터는 super().__init__(*args, *kwargs)로 써도 같은 효과를 얻을 수 있다. 해당 줄이 들어가 있지 않으면 클래스에 상속이 제대로 이루어지지 않는데, 이에 대해서 명확히 확인하고 싶다면, 아래 적어준 코드를 복붙해가서 직접 바꾸어가면서 실행해보면 좀 더 이해가 쉽다.

class A:
    def __init__(self, a="a", *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.a=a

    
class B(A):
    def __init__(self, b="b", *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.b=b
        self.hello='hello'
    
    def hi(self):
        print(f'hi {self.b}')
        
class OtherB(B):
    def __init__(self, b="otherB", *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.b=b

c = OtherB()
print(c.b) # otherB
c.hi() # hi otherB

여하튼 다시 본론으로 돌아가자면, 그 상태에서 원하는 field를 선택하고 해당 field의 attr을 update해주면 된다. Bootstrap을 사용한다고 가정하고 class에는 text-muted를 주었고, autofocus attribute는 False를 설정해보았다.

같은 효과인데, 그냥 복잡해보이지 않은 1-1의 방법을 쓰고 싶다고? 글쎄, 아마 이걸 보면 생각이 달라질지도 모른다.

class RestaurantForm(forms.ModelForm):
    class Meta:
        model = Restaurant
        fields = [
            "employee_num",
            "image",
            "location",
            "rating",
        ]

    def __init__(self, *args, **kwargs):
        super(RestuaranteForm, self).__init__(*args, **kwargs)
        for field in self.fields:
            print(field)
            new_attrs = {
                "class": "form-control"
            }
            self.fields[str(field)].widget.attrs.update(
                new_attrs
            )

반복해서 같은 class를 주거나, field들에 라벨을 연속적으로 붙여주는 일을 할 때에도 과연 이 방법을 사용하지 않을 수 있을까?
Bootstrap을 사용한다고 가정하고, 모든 input의 class로 'form-control'을 줘보았다.

아몰랑 다른 데에서 가져올래

(여기서부터는 제대로 써본 적이 없어 불확실하지만, 메모/정리의 의미로 적어둔 내용입니다. 나중에 외부 plugin을 사용하여 코딩을 하게 되는 경우가 생기면 점검/수정이 이루어지지 않을까 생각하고 있습니다)

아예 widget file을 따로 만들어서, custom widget을 사용할 수도 있다고 한다. 주로 외부 plugin을 사용할 때 사용되는 것 같았다.

(books/forms.py)

class BookForm(forms.ModelForm):
    class Meta:
        model = Book
        fields = [
            "title",
            "review",
            "rating",
        ]
        widgets = {"review": reviewWidget}

(books/widgets.py)

from django import forms

class reviewWidget(forms.TextInput):

    template_name = "widgets/custom_review.html"

    class Media:
        css = {
            "all": [
                "widgets/custom_review/style.css",
            ],
        }
        js = [
            "widgets/custom_review/review.ctrl.js",
        ]

    def build_attrs(self, *args, **kwargs):
        context = super(reviewWidget, self).build_attrs(*args, **kwargs)
        context.update(
            {
                "wrapper_class": "some-styled-wrapper-class",
                "class": "another-styled-class",
                "value": "",
                "name": "review"
            }
        )
        return context

class Media:를 설정해주면, django template에서 {{ bookForm.media }}로 불러올 수 있기 때문에 유용하다. 해당 파일들은 당연히 Django의 static 설정에 영향을 받으며, 따라서 두 파일은 모두 static/widgets/custom_review/ 폴더 하위에 저장되어야 한다.

(books/templates/widgets/custom_review.html)

<div class="{{ widget.context.wrapper_class }}">
    <textarea id="id-{{ widget.context.name }}" class="{{ widget.context.class }}" name="{{ widget.context.name }}">
        {% if widget.context.value %}
            {{ widget.context.value }}
        {% endif %}
    </textarea>
</div>

이 방식은 예시처럼 간단한 경우들 보다는 좀 복잡한 plugin들을 붙일 때 주로 쓰는 것 같다.


참고 사이트 :
파이썬 Super 명령 알아보기