Xây dựng và Xác thực Biểu mẫu với Django Forms

Tổng quan về Thành phần Form trong Django

Thành phần Form của Django cung cấp một giải pháp mạnh mẽ và hiệu quả để quản lý các biểu mẫu trên web. Nó giúp đơn giản hóa đáng kể quá trình xử lý biểu mẫu, từ việc tạo các phần tử HTML trên trang cho đến việc xác thực dữ liệu người dùng và giữ lại nội dung đã nhập trước đó.

Các chức năng chính của thành phần Form:

  • Tự động tạo các thẻ HTML cần thiết cho biểu mẫu.
  • Xác thực dữ liệu do người dùng gửi một cách an toàn và nhất quán.
  • Tự động điền lại các giá trị đã nhập trước đó khi có lỗi xác thực, cải thiện trải nghiệm người dùng.

Xử lý biểu mẫu theo cách thủ công

Trước khi đi sâu vào Django Forms, hãy xem xét cách xử lý một biểu mẫu đăng ký đơn giản bằng phương pháp truyền thống. Trong cách này, bạn tự tạo cấu trúc HTML cho biểu mẫu và quản lý logic xác thực cũng như xử lý dữ liệu hoàn toàn trong hàm view của Django.

views.py (Thủ công)

from django.http import HttpResponse
from django.shortcuts import render

def xu_ly_dang_ky_thu_cong(request):
    thong_bao_loi = ""
    if request.method == "POST":
        ten_nguoi_dung = request.POST.get("ten_tai_khoan")
        mat_khau = request.POST.get("mat_khau")
        # Kiểm tra cơ bản thông tin đăng ký
        if not ten_nguoi_dung or len(ten_nguoi_dung) < 6:
            thong_bao_loi = "Tên đăng nhập không được rỗng và phải có ít nhất 6 ký tự."
        elif not mat_khau:
            thong_bao_loi = "Mật khẩu không được rỗng."
        else:
            # Ở đây bạn sẽ lưu thông tin vào cơ sở dữ liệu
            return HttpResponse("Đăng ký tài khoản thành công!")
    return render(request, "bieu_mau_dang_ky_thu_cong.html", {"thong_bao_loi": thong_bao_loi})

bieu_mau_dang_ky_thu_cong.html

<!DOCTYPE html>
<html lang="vi">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Trang Đăng Ký Thủ Công</title>
    <style>
        .error-message { color: red; margin-top: 5px; }
        form div { margin-bottom: 15px; }
        label { display: block; margin-bottom: 5px; font-weight: bold; }
        input[type="text"], input[type="password"] {
            width: 300px;
            padding: 8px;
            border: 1px solid #ccc;
            border-radius: 4px;
        }
        input[type="submit"] {
            padding: 10px 20px;
            background-color: #007bff;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
    </style>
</head>
<body>
    <h1>Đăng Ký Tài Khoản (Cách Thủ Công)</h1>
    <form action="/dang-ky-thu-cong/" method="post">
        {% csrf_token %}
        <div>
            <label for="id_ten_tai_khoan">Tên Tài Khoản:</label>
            <input type="text" id="id_ten_tai_khoan" name="ten_tai_khoan">
        </div>
        <div>
            <label for="id_mat_khau">Mật Khẩu:</label>
            <input type="password" id="id_mat_khau" name="mat_khau">
        </div>
        <div>
            <input type="submit" value="Đăng Ký">
            {% if thong_bao_loi %}
                <p class="error-message">{{ thong_bao_loi }}</p>
            {% endif %}
        </div>
    </form>
</body>
</html>

Sử dụng Django Forms để thực hiện chức năng đăng ký

Với Django Forms, bạn định nghĩa cấu trúc biểu mẫu và các quy tắc xác thực trong một lớp Python riêng biệt. Sau đó, bạn truyền đối tượng biểu mẫu này tới template để hiển thị và để nó tự động xử lý dữ liệu gửi về.

forms.py

Thông thường, các lớp Form sẽ được đặt trong một file forms.py riêng biệt, cùng cấp với models.py hoặc trong một thư mục con của ứng dụng Django để dễ quản lý.

from django import forms

class BieuMauDangKyNguoiDung(forms.Form):
    ten_dang_nhap = forms.CharField(
        label="Tên đăng nhập",
        min_length=4,
        max_length=20,
        error_messages={
            'required': 'Vui lòng nhập tên đăng nhập.',
            'min_length': 'Tên đăng nhập tối thiểu 4 ký tự.',
            'max_length': 'Tên đăng nhập tối đa 20 ký tự.'
        }
    )
    mat_khau_nguoi_dung = forms.CharField(
        label="Mật khẩu",
        widget=forms.PasswordInput, # Sử dụng widget PasswordInput để hiển thị dấu *
        min_length=6,
        error_messages={
            'required': 'Vui lòng nhập mật khẩu.',
            'min_length': 'Mật khẩu tối thiểu 6 ký tự.'
        }
    )

views.py (Sử dụng Django Form)

from django.shortcuts import render
from django.http import HttpResponse
from .forms import BieuMauDangKyNguoiDung # Giả sử forms.py nằm trong cùng thư mục

def dang_ky_voi_django_form(request):
    if request.method == "POST":
        form_instance = BieuMauDangKyNguoiDung(request.POST) # Truyền dữ liệu POST vào form
        if form_instance.is_valid():
            # Dữ liệu đã được xác thực nằm trong form_instance.cleaned_data
            # print(form_instance.cleaned_data)
            # Thực hiện lưu dữ liệu vào cơ sở dữ liệu hoặc xử lý khác
            return HttpResponse("Đăng ký thành công bằng Django Form!")
    else:
        form_instance = BieuMauDangKyNguoiDung() # Khởi tạo form trống cho GET request

    return render(request, "bieu_mau_dang_ky_form_django.html", {"form_data": form_instance})

bieu_mau_dang_ky_form_django.html

<!DOCTYPE html>
<html lang="vi">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Đăng ký với Django Form</title>
    <style>
        .error-list { color: red; margin-top: 5px; list-style-type: none; padding-left: 0; }
        .error-list li { margin-bottom: 3px; }
        form div { margin-bottom: 15px; }
        label { display: block; margin-bottom: 5px; font-weight: bold; }
        input[type="text"], input[type="password"] {
            width: 300px;
            padding: 8px;
            border: 1px solid #ccc;
            border-radius: 4px;
        }
        .btn-submit {
            padding: 10px 20px;
            background-color: #28a745;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
    </style>
</head>
<body>
    <h1>Đăng Ký Tài Khoản (Sử dụng Django Form)</h1>
    <form action="/dang-ky-voi-form/" method="post" novalidate autocomplete="off">
        {% csrf_token %}
        <div>
            <label for="{{ form_data.ten_dang_nhap.id_for_label }}">{{ form_data.ten_dang_nhap.label }}:</label>
            {{ form_data.ten_dang_nhap }}
            {% if form_data.ten_dang_nhap.errors %}
                <ul class="error-list">
                    {% for error in form_data.ten_dang_nhap.errors %}
                        <li>{{ error }}</li>
                    {% endfor %}
                </ul>
            {% endif %}
        </div>
        <div>
            <label for="{{ form_data.mat_khau_nguoi_dung.id_for_label }}">{{ form_data.mat_khau_nguoi_dung.label }}:</label>
            {{ form_data.mat_khau_nguoi_dung }}
            {% if form_data.mat_khau_nguoi_dung.errors %}
                <ul class="error-list">
                    {% for error in form_data.mat_khau_nguoi_dung.errors %}
                        <li>{{ error }}</li>
                    {% endfor %}
                </ul>
            {% endif %}
        </div>
        <div>
            <input type="submit" class="btn-submit" value="Đăng Ký">
        </div>
    </form>
</body>
</html>

Các Trường và Widget Phổ biến của Django Form

Django cung cấp nhiều loại trường và widget để tùy chỉnh biểu mẫu của bạn.

from django import forms
from django.forms import widgets, fields

class CauHinhNguoiDungForm(forms.Form):
    # 'label': Nhãn hiển thị cho trường.
    ten_tai_khoan = forms.CharField(
        min_length=5,
        label="Tên Người Dùng",
        max_length=50,
        help_text="Tên người dùng phải có ít nhất 5 ký tự."
    )

    # 'initial': Giá trị mặc định ban đầu của trường trong biểu mẫu.
    email_lien_he = forms.EmailField(
        label="Email Liên Hệ",
        initial="example@domain.com",
        required=True
    )

    # 'error_messages': Tùy chỉnh thông báo lỗi cho các loại lỗi xác thực cụ thể.
    mat_khau_moi = forms.CharField(
        min_length=8,
        label="Mật khẩu mới",
        widget=widgets.PasswordInput, # Sử dụng widget để che mật khẩu
        error_messages={
            "required": "Trường mật khẩu không được để trống.",
            "min_length": "Mật khẩu phải có ít nhất 8 ký tự."
        }
    )

    # 'widget': Một đối tượng Widget để tùy chỉnh cách trường được hiển thị trong HTML.
    # attrs={'k':'v'} cho phép thêm các thuộc tính HTML tùy chỉnh (ví dụ: class, id, placeholder).
    # render_value=True giữ lại giá trị đã nhập khi có lỗi xác thực (thường dùng cho PasswordInput).
    xac_nhan_mat_khau = forms.CharField(
        label="Xác nhận mật khẩu",
        widget=widgets.PasswordInput(attrs={'class': 'input-password-confirm'}, render_value=True),
        help_text="Nhập lại mật khẩu mới của bạn."
    )

    # 'RadioSelect': Widget cho các lựa chọn radio (chỉ chọn một).
    # 'choices': Một iterable gồm các cặp (giá_trị_thực_tế, nhãn_hiển_thị).
    loai_tai_khoan = fields.ChoiceField(
        choices=(('free', "Miễn phí"), ('premium', "Cao cấp"), ('vip', "VIP")),
        label="Loại Tài Khoản",
        initial='free', # Giá trị mặc định được chọn
        widget=widgets.RadioSelect
    )

    # 'SelectMultiple': Widget cho phép chọn nhiều mục từ danh sách thả xuống.
    ngon_ngu_yeu_thich = fields.MultipleChoiceField(
        choices=(('py', "Python"), ('js', "JavaScript"), ('java', "Java"), ('cpp', "C++")),
        label="Ngôn Ngữ Yêu Thích",
        initial=['py', 'js'], # Các giá trị mặc định được chọn (dạng list)
        widget=widgets.SelectMultiple(attrs={'size': '5'}) # Thuộc tính size cho phép hiển thị nhiều dòng
    )

    # 'CheckboxInput': Widget cho một hộp kiểm duy nhất.
    dong_y_dieu_khoan = fields.BooleanField(
        label="Đồng ý với các điều khoản dịch vụ",
        initial=True, # Mặc định được chọn
        required=True,
        widget=widgets.CheckboxInput()
    )

    # 'CheckboxSelectMultiple': Widget cho nhiều hộp kiểm.
    thong_bao_muon_nhan = fields.MultipleChoiceField(
        choices=(('email', "Qua Email"), ('sms', "Qua SMS"), ('app', "Trong Ứng Dụng")),
        label="Thông Báo Muốn Nhận",
        initial=['email'], # Các giá trị mặc định được chọn (dạng list)
        widget=widgets.CheckboxSelectMultiple
    )

Lưu ý về trường ChoiceField

Các trường lựa chọn (ChoiceField, ModelChoiceField, v.v.) thường có các tùy chọn được định nghĩa tĩnh. Tuy nhiên, trong nhiều trường hợp, bạn muốn các tùy chọn này được lấy từ cơ sở dữ liệu và cần cập nhật theo thời gian thực. Để làm điều này, bạn có thể ghi đè phương thức khởi tạo __init__ của lớp Form.

Cách 1: Ghi đè phương thức __init__

Trong phương thức __init__, bạn gọi super().__init__(*args, **kwargs) để đảm bảo lớp cha được khởi tạo đúng cách, sau đó bạn có thể tùy chỉnh thuộc tính choices của trường bằng cách truy vấn cơ sở dữ liệu hoặc nguồn dữ liệu động khác.

from django.forms import Form, widgets, fields
# from .models import Category # Nếu bạn lấy dữ liệu từ một model thực tế

class FormTuyChonDong(Form):
    phan_loai = fields.ChoiceField(
        label="Chọn Phân Loại",
        widget=widgets.Select
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Ví dụ: Lấy dữ liệu từ một danh sách tĩnh hoặc từ DB
        # self.fields['phan_loai'].choices = Category.objects.all().values_list('id', 'name')
        # Hoặc một ví dụ cụ thể hơn với choices lấy từ một list
        categories_data = [
            (101, 'Sản Phẩm Điện Tử'),
            (102, 'Sách và Văn Phòng Phẩm'),
            (103, 'Thời Trang')
        ]
        self.fields['phan_loai'].choices = categories_data
        self.fields['phan_loai'].initial = 101 # Giá trị khởi tạo

Cách 2: Sử dụng ModelChoiceFieldModelMultipleChoiceField

Django cung cấp các trường đặc biệt ModelChoiceField (chọn một đối tượng) và ModelMultipleChoiceField (chọn nhiều đối tượng) giúp bạn dễ dàng liên kết trực tiếp với QuerySet của các model. Các trường này tự động tạo các tùy chọn từ các đối tượng trong QuerySet được cung cấp.

from django import forms
from django.forms import models as form_models_fields
# from .models import Author, Book # Giả sử bạn có các model Author và Book

class BieuMauDuLieuDoiTuong(forms.Form):
    # ModelChoiceField cho phép người dùng chọn một đối tượng từ QuerySet
    sach_yeu_thich = form_models_fields.ModelChoiceField(
        # queryset=Book.objects.filter(is_published=True), # Lấy sách đã xuất bản
        queryset=[], # Placeholder, sẽ được điền trong __init__
        empty_label="--- Chọn một cuốn sách ---",
        label="Cuốn Sách Ưa Thích"
    )
    # ModelMultipleChoiceField cho phép người dùng chọn nhiều đối tượng từ QuerySet
    cac_tac_gia_tham_gia = form_models_fields.ModelMultipleChoiceField(
        # queryset=Author.objects.all().order_by('name'), # Lấy tất cả tác giả
        queryset=[], # Placeholder, sẽ được điền trong __init__
        label="Các Tác Giả Tham Gia",
        widget=forms.SelectMultiple
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Để minh họa mà không cần model thực tế, tạo các đối tượng giả (dummy objects)
        class DummyAuthor:
            def __init__(self, id, name): self.id, self.name = id, name
            def __str__(self): return self.name
        class DummyBook:
            def __init__(self, id, title): self.id, self.title = id, title
            def __str__(self): return self.title

        dummy_authors_list = [DummyAuthor(1, "Nguyễn Nhật Ánh"), DummyAuthor(2, "Haruki Murakami")]
        dummy_books_list = [DummyBook(1, "Mắt Biếc"), DummyBook(2, "Rừng Na Uy")]

        self.fields['cac_tac_gia_tham_gia'].queryset = dummy_authors_list
        self.fields['sach_yeu_thich'].queryset = dummy_books_list

Các Trường Form Tích hợp sẵn của Django

Dưới đây là danh sách các trường form cơ bản được cung cấp bởi Django, cùng với các thuộc tính phổ biến của chúng:

Field (Lớp cơ sở cho tất cả các trường)
    required=True,               # Trường có bắt buộc phải nhập hay không
    widget=None,                 # Widget HTML dùng để hiển thị trường
    label=None,                  # Nhãn hiển thị cho trường
    initial=None,                # Giá trị ban đầu của trường
    help_text='',                # Văn bản trợ giúp (hiển thị gần nhãn)
    error_messages=None,         # Từ điển tùy chỉnh thông báo lỗi, ví dụ: {'required': 'Không thể bỏ trống'}
    validators=[],               # Danh sách các hàm xác thực tùy chỉnh
    localize=False,              # Có hỗ trợ bản địa hóa dữ liệu không
    disabled=False,              # Trường có bị vô hiệu hóa (không thể chỉnh sửa) không
    label_suffix=None            # Hậu tố cho nhãn

CharField(Field)
    max_length=None,             # Độ dài tối đa của chuỗi
    min_length=None,             # Độ dài tối thiểu của chuỗi
    strip=True                   # Có xóa khoảng trắng đầu/cuối của dữ liệu nhập không

IntegerField(Field)
    max_value=None,              # Giá trị số nguyên tối đa
    min_value=None,              # Giá trị số nguyên tối thiểu

FloatField(IntegerField)         # Tương tự IntegerField nhưng cho số thực

DecimalField(IntegerField)
    max_value=None,              # Giá trị thập phân tối đa
    min_value=None,              # Giá trị thập phân tối thiểu
    max_digits=None,             # Tổng số chữ số (bao gồm cả phần thập phân)
    decimal_places=None,         # Số chữ số sau dấu thập phân

BaseTemporalField(Field)
    input_formats=None           # Định dạng thời gian/ngày tháng đầu vào

DateField(BaseTemporalField)     # Định dạng: YYYY-MM-DD
TimeField(BaseTemporalField)     # Định dạng: HH:MM:SS
DateTimeField(BaseTemporalField) # Định dạng: YYYY-MM-DD HH:MM:SS

DurationField(Field)             # Khoảng thời gian (ví dụ: "DD HH:MM:SS")

RegexField(CharField)
    regex,                       # Biểu thức chính quy tùy chỉnh để xác thực
    max_length=None,
    min_length=None,
    # error_message=None,        # Sử dụng error_messages={'invalid': '...'} thay thế

EmailField(CharField)            # Xác thực định dạng email

FileField(Field)
    allow_empty_file=False       # Có cho phép upload file rỗng không
ImageField(FileField)            # Yêu cầu thư viện Pillow (pip install Pillow)
    # Lưu ý khi sử dụng FileField/ImageField:
    # - Form HTML cần có: enctype="multipart/form-data"
    # - Hàm view cần xử lý: form_instance = MyForm(request.POST, request.FILES)

URLField(Field)                  # Xác thực định dạng URL

BooleanField(Field)              # Trường boolean (True/False)

NullBooleanField(BooleanField)   # Trường boolean cho phép giá trị None (True/False/None)

ChoiceField(Field)
    choices=(),                  # Các tùy chọn lựa chọn, ví dụ: ((0,'Tùy chọn A'),(1,'Tùy chọn B'))
    required=True,
    widget=None,                 # Mặc định là Select widget
    label=None,
    initial=None,
    help_text=''

ModelChoiceField(ChoiceField)    # Chọn một đối tượng từ QuerySet
    queryset,                    # QuerySet để lấy các đối tượng lựa chọn
    empty_label="---------",     # Nhãn cho tùy chọn rỗng
    to_field_name=None,          # Trường của model sẽ được dùng làm giá trị HTML 'value'
    limit_choices_to=None        # Điều kiện lọc bổ sung cho QuerySet trong ModelForm

ModelMultipleChoiceField(ModelChoiceField) # Chọn nhiều đối tượng từ QuerySet

TypedChoiceField(ChoiceField)
    coerce = lambda val: val     # Hàm chuyển đổi giá trị đã chọn
    empty_value= ''              # Giá trị mặc định cho trường rỗng

MultipleChoiceField(ChoiceField) # Chọn nhiều giá trị từ các tùy chọn

TypedMultipleChoiceField(MultipleChoiceField)
    coerce = lambda val: val     # Hàm chuyển đổi từng giá trị đã chọn
    empty_value= ''

ComboField(Field)
    fields=()                    # Sử dụng nhiều xác thực kết hợp, ví dụ: [CharField(max_length=20), EmailField()]

MultiValueField(Field)           # Lớp trừu tượng, dùng để kết hợp nhiều trường thành một giá trị. Phối hợp với MultiWidget.

SplitDateTimeField(MultiValueField) # Tách DateTime thành trường ngày và trường giờ riêng biệt
    input_date_formats=None,
    input_time_formats=None

FilePathField(ChoiceField)       # Cung cấp các lựa chọn từ các file trong một thư mục
    path,                        # Đường dẫn thư mục
    match=None,                  # Biểu thức chính quy để lọc file
    recursive=False,             # Có tìm kiếm trong các thư mục con không
    allow_files=True,            # Cho phép chọn file
    allow_folders=False,         # Cho phép chọn thư mục

GenericIPAddressField
    protocol='both',             # 'both', 'ipv4', 'ipv6'
    unpack_ipv4=False            # Giải nén địa chỉ IPv4 từ địa chỉ IPv6 (ví dụ: ::ffff:192.0.2.1 thành 192.0.2.1)

SlugField(CharField)             # Chỉ cho phép chữ, số, dấu gạch dưới, dấu gạch ngang

UUIDField(CharField)             # Xác thực định dạng UUID

Các Phương pháp Hiển thị Thẻ của Django Form

Django Forms cung cấp nhiều cách để hiển thị biểu mẫu trong template, từ tự động hoàn toàn đến tùy chỉnh thủ công từng phần tử.

Hiển thị mặc định toàn bộ Form

Django cung cấp các phương thức tiện lợi để hiển thị toàn bộ các trường của form một cách nhanh chóng, theo các định dạng HTML phổ biến. Điều này giúp thao tác đơn giản nhưng có thể thêm các thuộc tính HTML không cần thiết và khó kiểm soát bố cục toàn diện.

  • {{ form_data_display.as_p }}: Hiển thị mỗi trường trong một thẻ <p>.
  • {{ form_data_display.as_ul }}: Hiển thị mỗi trường trong một thẻ <li> (bên trong thẻ <ul>).
  • {{ form_data_display.as_table }}: Hiển thị mỗi trường trong một hàng bảng (<tr>).

Tự viết cấu trúc và hiển thị từng trường

Bạn có thể tự viết cấu trúc HTML cơ bản cho biểu mẫu của mình và sau đó sử dụng các biến của form để hiển thị từng trường. Cách này cung cấp khả năng kiểm soát bố cục tốt hơn nhưng vẫn tận dụng được các thuộc tính do Django Form cung cấp.

<!-- Ví dụ từ bieu_mau_dang_ky_form_django.html -->
<form action="/dang-ky-voi-form/" method="post" novalidate autocomplete="off">
    {% csrf_token %}
    <div>
        <label for="{{ form_data.ten_dang_nhap.id_for_label }}">{{ form_data.ten_dang_nhap.label }}:</label>
        {{ form_data.ten_dang_nhap }}
        {% if form_data.ten_dang_nhap.errors %}
            <ul class="error-list">
                {% for error in form_data.ten_dang_nhap.errors %}
                    <li>{{ error }}</li>
                {% endfor %}
            </ul>
        {% endif %}
    </div>
    <!-- Các trường khác tương tự -->
    <div>
        <input type="submit" class="btn-submit" value="Đăng Ký">
    </div>
</form>

Hoàn toàn viết thủ công HTML và chỉ dùng Form để xác thực

Trong một số trường hợp, bạn muốn có toàn quyền kiểm soát cấu trúc HTML của mình. Khi đó, bạn có thể viết thủ công tất cả các thẻ HTML cho biểu mẫu và chỉ sử dụng đối tượng Django Form để xác thực dữ liệu gửi về.

Lưu ý quan trọng: Khi tự viết HTML, thuộc tính name của các thẻ <input> phải khớp chính xác với tên trường được định nghĩa trong lớp Django Form của bạn để quá trình xác thực hoạt động.

bieu_mau_them_sach.html (HTML tự viết)

<!DOCTYPE html>
<html lang="vi">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Thêm Sách Tùy Chỉnh</title>
</head>
<body>
    <h1>Thêm Sách (Tùy Chỉnh HTML)</h1>
    <form action="/them-sach-tu-do/" method="post">
        {% csrf_token %}
        <p>Tên Sách: <input type="text" name="ten_sach_form" id="id_ten_sach_form"></p>
        <p>Giá Bán: <input type="number" name="gia_ban_form" id="id_gia_ban_form" step="0.01"></p>
        <input type="submit" value="Lưu Sách">
        {% if form_sach.errors %}
            <div style="color: red;">
                <p>Có lỗi trong biểu mẫu:</p>
                <ul>
                    {% for field, errors in form_sach.errors.items %}
                        <li>{{ field }}: {{ errors|join:", " }}</li>
                    {% endfor %}
                </ul>
            </div>
        {% endif %}
    </form>
</body>
</html>

views.py (Form xác thực HTML tự viết)

from django.forms import Form
from django import forms
from django.shortcuts import render

class BieuMauSachTuyChinh(Form):
    ten_sach_form = forms.CharField(label="Tên sách", max_length=100,
                                    error_messages={'required': 'Tên sách không được rỗng.'})
    gia_ban_form = forms.FloatField(label="Giá bán", min_value=0.01,
                                    error_messages={'required': 'Giá bán không được rỗng.',
                                                    'min_value': 'Giá bán phải lớn hơn 0.'})

def them_sach_bang_html_tu_do(request):
    form_sach = BieuMauSachTuyChinh() # Khởi tạo form trống cho GET request
    if request.method == "POST":
        # Truyền request.POST trực tiếp vào form để nó xử lý
        form_sach = BieuMauSachTuyChinh(request.POST)
        if form_sach.is_valid():
            # Dữ liệu hợp lệ đã nằm trong form_sach.cleaned_data
            print("Dữ liệu đã xác thực:", form_sach.cleaned_data)
            # Thực hiện lưu vào DB hoặc xử lý khác
            return HttpResponse("Sách đã được thêm thành công!")
        else:
            # Nếu có lỗi, form_sach.errors sẽ chứa thông tin lỗi
            print("Lỗi xác thực:", form_sach.errors)
    return render(request, "bieu_mau_them_sach.html", {'form_sach': form_sach})

Khi xác thực, nếu lớp Form yêu cầu ít trường hơn so với dữ liệu POST thực tế, các trường thừa sẽ bị bỏ qua mà không gây lỗi (ví dụ: trường csrf_token). Ngược lại, nếu Form yêu cầu nhiều trường hơn dữ liệu POST, các trường thiếu sẽ bị báo lỗi thiếu dữ liệu.

Ghi đè Hàm Hook Xác thực

Django Forms cho phép bạn thêm logic xác thực tùy chỉnh bằng cách ghi đè các hàm "hook" (móc nối) nhất định.

from django import forms
from django.core.exceptions import ValidationError

class BieuMauDangKyNangCao(forms.Form):
    ten_nguoi_dung_hook = forms.CharField(label="Tên Người Dùng", max_length=30)
    mat_khau_hook = forms.CharField(label="Mật Khẩu", widget=forms.PasswordInput)
    xac_nhan_mat_khau_hook = forms.CharField(label="Xác Nhận Mật Khẩu", widget=forms.PasswordInput)
    ma_xac_nhan_tu_dong = forms.CharField(label="Mã Xác Nhận", max_length=6, min_length=6)

    # Hook cục bộ: `clean_<field_name>`
    # Hàm này được gọi sau khi các trình xác thực mặc định của trường đã chạy.
    # Nó dùng để xác thực riêng cho trường đó hoặc điều chỉnh giá trị của nó.
    def clean_ten_nguoi_dung_hook(self):
        gia_tri_ten = self.cleaned_data.get("ten_nguoi_dung_hook")
        # Ví dụ: cấm các từ nhạy cảm
        if "admin" in gia_tri_ten.lower() or "root" in gia_tri_ten.lower():
            raise ValidationError("Tên người dùng không được chứa các từ cấm như 'admin' hoặc 'root'!")
        # Bạn có thể kiểm tra sự tồn tại của tên người dùng trong DB ở đây
        # if User.objects.filter(username=gia_tri_ten).exists():
        #     raise ValidationError("Tên người dùng này đã tồn tại.")
        return gia_tri_ten # Luôn phải trả về giá trị đã làm sạch

    # Hook toàn cục: `clean()`
    # Hàm này được gọi sau khi tất cả các trình xác thực trường và các hook cục bộ đã chạy.
    # Nó dùng để xác thực các phụ thuộc giữa các trường hoặc các kiểm tra tổng thể.
    def clean(self):
        # Quan trọng: Gọi phương thức clean của lớp cha để đảm bảo các kiểm tra cơ bản đã được thực hiện
        super().clean()

        mat_khau = self.cleaned_data.get("mat_khau_hook")
        xac_nhan_mat_khau = self.cleaned_data.get("xac_nhan_mat_khau_hook")
        ma_xac_nhan = self.cleaned_data.get("ma_xac_nhan_tu_dong")

        # Kiểm tra trùng khớp mật khẩu
        if mat_khau and xac_nhan_mat_khau and mat_khau != xac_nhan_mat_khau:
            # add_error() được dùng để thêm lỗi vào một trường cụ thể
            self.add_error("xac_nhan_mat_khau_hook", ValidationError("Mật khẩu xác nhận không khớp."))

        # Ví dụ kiểm tra mã xác nhận đơn giản (trong thực tế sẽ phức tạp hơn)
        # Giả định mã xác nhận đúng là "SECRET"
        if ma_xac_nhan and ma_xac_nhan != "SECRET":
            self.add_error("ma_xac_nhan_tu_dong", ValidationError("Mã xác nhận không đúng."))
        
        # Luôn phải trả về self.cleaned_data
        return self.cleaned_data

Trình tự thực thi của phương thức form.is_valid()

Khi bạn gọi form.is_valid(), Django thực hiện một chuỗi các bước xác thực nội bộ. Hiểu rõ trình tự này giúp bạn biết khi nào và ở đâu để can thiệp bằng các hàm hook tùy chỉnh.

  1. Kiểm tra ban đầu: Phương thức is_valid() đầu tiên kiểm tra xem form đã được khởi tạo với dữ liệu (thường là request.POST hoặc request.FILES) và liệu nó có lỗi chưa được xử lý không.
  2. Gọi full_clean(): Đây là phương thức chính chịu trách nhiệm cho toàn bộ quá trình xác thực.
  3. Khởi tạo đối tượng lưu trữ lỗi và dữ liệu:
    • Một từ điển rỗng self._errors (thuộc kiểu ErrorDict) được tạo để lưu trữ tất cả các lỗi được tìm thấy.
    • Một từ điển rỗng self.cleaned_data được tạo để lưu trữ dữ liệu đã được xác thực thành công.
  4. Thực thi _clean_fields():
    • Lặp qua từng trường được định nghĩa trong form (self.fields.items()).
    • Với mỗi trường, nó chạy các trình xác thực tích hợp sẵn của trường đó (ví dụ: min_length, max_length, định dạng EmailField).
    • Nếu có lỗi, một đối tượng ValidationError sẽ được thêm vào self._errors.
    • Nếu không có lỗi, giá trị đã được làm sạch của trường sẽ được thêm vào self.cleaned_data.
    • Sau khi chạy trình xác thực mặc định, Django kiểm tra xem có phương thức hook cục bộ nào có tên clean_<tên_trường> (ví dụ: clean_ten_nguoi_dung_hook) hay không và gọi nó. Lỗi từ hook này cũng được thêm vào self._errors, và giá trị trả về của hook sẽ cập nhật self.cleaned_data cho trường đó.
  5. Thực thi _clean_form():
    • Phương thức này gọi hook toàn cục self.clean() (nếu bạn đã ghi đè nó).
    • Trong hook clean(), bạn có thể thực hiện các kiểm tra xác thực liên quan đến nhiều trường (ví dụ: so sánh mật khẩu và xác nhận mật khẩu).
    • Bất kỳ lỗi nào được thêm bằng self.add_error() trong clean() sẽ được lưu vào self._errors.
    • Phương thức clean() phải trả về self.cleaned_data (có thể đã được sửa đổi).
  6. Thực thi post_clean():
    • Đây là một hook cuối cùng được gọi sau tất cả các quá trình xác thực khác. Nó thường được sử dụng cho các xử lý sau xác thực mà không liên quan trực tiếp đến việc thêm lỗi mới vào form (ví dụ: loại bỏ các trường không cần thiết từ cleaned_data, hoặc các tác vụ phụ khác).
  7. Kết quả:
    • Nếu self._errors rỗng sau tất cả các bước này, form được coi là hợp lệ (is_valid() trả về True).
    • Nếu self._errors chứa bất kỳ lỗi nào, form không hợp lệ (is_valid() trả về False).

Như vậy, cả hook cục bộ (clean_<tên_trường>) và hook toàn cục (clean()) đều thao tác trên dữ liệu đã được xác thực một phần hoặc hoàn chỉnh có trong self.cleaned_data. Điều này cho phép bạn kiểm tra và thay đổi dữ liệu sau khi các trình xác thực cơ bản đã hoạt động.

Thẻ: Django forms python Form Validation Widgets

Đăng vào ngày 11 tháng 8 lúc 17:17