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