Form, ModelForm, валидация, виджеты, formsets, crispy-forms
Формы в Django — это мощный инструмент для обработки пользовательского ввода, валидации данных и сохранения в базу данных.
💡 Правило: Всегда валидируйте данные на сервере, даже если есть клиентская валидация. Доверяй, но проверяй.
# forms.py
from django import forms
class ContactForm(forms.Form):
name = forms.CharField(
max_length=100,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Ваше имя'
})
)
email = forms.EmailField(
widget=forms.EmailInput(attrs={
'class': 'form-control',
'placeholder': 'Email'
})
)
message = forms.CharField(
widget=forms.Textarea(attrs={
'class': 'form-control',
'placeholder': 'Сообщение',
'rows': 5
})
)
subscribe = forms.BooleanField(
required=False,
widget=forms.CheckboxInput(attrs={'class': 'form-check-input'})
)# views.py
from django.shortcuts import render, redirect
from .forms import ContactForm
def contact_view(request):
if request.method == 'POST':
form = ContactForm(request.POST)
if form.is_valid():
# Обработка валидных данных
name = form.cleaned_data['name']
email = form.cleaned_data['email']
message = form.cleaned_data['message']
# Отправка email
send_mail(
subject=f'Contact from {name}',
message=message,
from_email=email,
recipient_list=['admin@example.com'],
)
return redirect('contact_success')
else:
form = ContactForm()
return render(request, 'contact.html', {'form': form})<!-- contact.html -->
<form method="post" novalidate>
{% csrf_token %}
<!-- Ручной рендеринг -->
<div class="form-group">
{{ form.name.label_tag }}
{{ form.name }}
{% if form.name.errors %}
<div class="error">{{ form.name.errors }}</div>
{% endif %}
</div>
<div class="form-group">
{{ form.email.label_tag }}
{{ form.email }}
{% if form.email.errors %}
<div class="error">{{ form.email.errors }}</div>
{% endif %}
</div>
<div class="form-group">
{{ form.message.label_tag }}
{{ form.message }}
{% if form.message.errors %}
<div class="error">{{ form.message.errors }}</div>
{% endif %}
</div>
<div class="form-check">
{{ form.subscribe }}
{{ form.subscribe.label_tag }}
</div>
<button type="submit" class="btn btn-primary">Отправить</button>
</form>
<!-- Или автоматический рендеринг -->
<form method="post" novalidate>
{% csrf_token %}
{{ form.as_p }} <!-- В параграфах -->
{{ form.as_table }} <!-- В таблице -->
{{ form.as_ul }} <!-- В списке -->
<button type="submit">Отправить</button>
</form># forms.py
from django import forms
from blog.models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'slug', 'content', 'category', 'tags']
# Или fields = '__all__' для всех полей
# exclude = ['author', 'created_at'] # Исключить поля
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Кастомизация виджетов
self.fields['title'].widget.attrs.update({
'class': 'form-control',
'placeholder': 'Заголовок поста'
})
self.fields['content'].widget.attrs.update({
'class': 'form-control',
'rows': 10
})
self.fields['category'].widget.attrs.update({
'class': 'form-select'
})# views.py
from django.shortcuts import render, redirect, get_object_or_404
from .forms import PostForm
from .models import Post
def post_create(request):
if request.method == 'POST':
form = PostForm(request.POST)
if form.is_valid():
post = form.save(commit=False) # Не сохранять сразу
post.author = request.user # Установить автора
post.save() # Теперь сохранить
form.save_m2m() # Сохранить ManyToMany (tags)
return redirect('post_detail', pk=post.pk)
else:
form = PostForm()
return render(request, 'posts/form.html', {'form': form})
def post_edit(request, pk):
post = get_object_or_404(Post, pk=pk)
if request.method == 'POST':
form = PostForm(request.POST, instance=post)
if form.is_valid():
form.save()
return redirect('post_detail', pk=post.pk)
else:
form = PostForm(instance=post)
return render(request, 'posts/form.html', {'form': form})class RegistrationForm(forms.Form):
username = forms.CharField(max_length=100)
email = forms.EmailField()
password = forms.CharField(widget=forms.PasswordInput)
password_confirm = forms.CharField(widget=forms.PasswordInput)
def clean_username(self):
username = self.cleaned_data['username']
# Проверка длины
if len(username) < 3:
raise forms.ValidationError('Username must be at least 3 characters')
# Проверка на существование
if User.objects.filter(username=username).exists():
raise forms.ValidationError('Username already taken')
# Проверка формата
if not username.isalnum():
raise forms.ValidationError('Username can only contain letters and numbers')
return username
def clean_email(self):
email = self.cleaned_data['email']
# Проверка домена
allowed_domains = ['example.com', 'company.com']
domain = email.split('@')[1]
if domain not in allowed_domains:
raise forms.ValidationError(f'Email must be from {allowed_domains}')
return email
def clean_password(self):
password = self.cleaned_data['password']
# Проверка сложности пароля
if len(password) < 8:
raise forms.ValidationError('Password must be at least 8 characters')
if not any(c.isupper() for c in password):
raise forms.ValidationError('Password must contain uppercase letter')
if not any(c.isdigit() for c in password):
raise forms.ValidationError('Password must contain a digit')
return password
def clean(self):
cleaned_data = super().clean()
password = cleaned_data.get('password')
password_confirm = cleaned_data.get('password_confirm')
# Проверка совпадения паролей
if password and password_confirm and password != password_confirm:
raise forms.ValidationError('Passwords do not match')
return cleaned_datafrom django.core.validators import MinLengthValidator, MaxLengthValidator, RegexValidator
from django.core.exceptions import ValidationError
def validate_even(value):
"""Кастомный валидатор."""
if value % 2 != 0:
raise ValidationError('Value must be even')
class ProductForm(forms.Form):
name = forms.CharField(
max_length=100,
validators=[
MinLengthValidator(3, 'Name must be at least 3 characters'),
MaxLengthValidator(100, 'Name must be at most 100 characters'),
]
)
code = forms.CharField(
max_length=20,
validators=[
RegexValidator(
regex=r'^[A-Z]{2}-\d{4}$',
message='Code must be in format XX-0000 (e.g., AB-1234)'
)
]
)
quantity = forms.IntegerField(validators=[validate_even])from django import forms
class ExampleForm(forms.Form):
# Текстовые поля
text_input = forms.CharField(widget=forms.TextInput())
text_area = forms.CharField(widget=forms.Textarea())
email = forms.EmailField(widget=forms.EmailInput())
url = forms.URLField(widget=forms.URLInput())
password = forms.CharField(widget=forms.PasswordInput())
# Числовые поля
integer = forms.IntegerField(widget=forms.NumberInput())
decimal = forms.DecimalField(widget=forms.DecimalNumberInput())
float_field = forms.FloatField(widget=forms.NumberInput())
# Дата и время
date = forms.DateField(widget=forms.DateInput())
time = forms.TimeField(widget=forms.TimeInput())
datetime = forms.DateTimeField(widget=forms.DateTimeInput())
# Выбор
choice = forms.ChoiceField(widget=forms.Select(choices=[('1', 'Option 1'), ('2', 'Option 2')]))
radio = forms.ChoiceField(widget=forms.RadioSelect(choices=[('1', 'Option 1'), ('2', 'Option 2')]))
checkbox = forms.BooleanField(widget=forms.CheckboxInput())
checkbox_multiple = forms.MultipleChoiceField(
widget=forms.CheckboxSelectMultiple(choices=[('1', 'Option 1'), ('2', 'Option 2')])
)
# Файлы
file = forms.FileField(widget=forms.FileInput())
image = forms.ImageField(widget=forms.ClearableFileInput())
# Скрытые поля
hidden = forms.CharField(widget=forms.HiddenInput())class CustomDateInput(forms.DateInput):
"""Виджет для выбора даты с HTML5 date picker."""
input_type = 'date'
def __init__(self, attrs=None, format=None):
default_attrs = {
'class': 'form-control',
'type': 'date'
}
if attrs:
default_attrs.update(attrs)
super().__init__(attrs=default_attrs, format=format)
class TagInput(forms.TextInput):
"""Виджет для ввода тегов через Select2."""
class Media:
css = {
'all': ('https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css',)
}
js = (
'https://code.jquery.com/jquery-3.6.0.min.js',
'https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js',
)
def render(self, name, value, attrs=None, renderer=None):
html = super().render(name, value, attrs, renderer)
script = f"""
<script>
$(document).ready(function() {{
$('#id_{name}').select2({{
tags: true,
tokenSeparators: [',', ' ']
}});
}});
</script>
"""
return html + script
class EventForm(forms.Form):
date = forms.DateField(widget=CustomDateInput())
tags = forms.CharField(widget=TagInput())from django.forms import formset_factory
from .forms import PostForm
# Создание formset
PostFormSet = formset_factory(PostForm, extra=3, max_num=10, validate_max=True)
def create_posts(request):
if request.method == 'POST':
formset = PostFormSet(request.POST)
if formset.is_valid():
for form in formset:
if form.cleaned_data: # Пропустить пустые формы
title = form.cleaned_data['title']
Post.objects.create(title=title)
return redirect('post_list')
else:
formset = PostFormSet()
return render(request, 'posts/formset.html', {'formset': formset})from django.forms.models import inlineformset_factory
from blog.models import Author, Post
# Создание inline formset
PostFormSet = inlineformset_factory(
Author,
Post,
fields=['title', 'content'],
extra=2,
can_delete=True,
max_num=10,
validate_max=True
)
def author_edit(request, pk):
author = get_object_or_404(Author, pk=pk)
if request.method == 'POST':
formset = PostFormSet(request.POST, instance=author)
if formset.is_valid():
formset.save()
return redirect('author_detail', pk=author.pk)
else:
formset = PostFormSet(instance=author)
return render(request, 'authors/edit.html', {
'author': author,
'formset': formset
})<!-- formset.html -->
<form method="post">
{% csrf_token %}
{{ formset.management_form }}
{% for form in formset %}
<div class="formset-item">
<h4>Form {{ forloop.counter }}</h4>
{{ form.as_p }}
{% if formset.can_delete %}
<label>
{{ form.DELETE }} Delete
</label>
{% endif %}
</div>
{% endfor %}
<button type="submit">Save</button>
</form>pip install django-crispy-forms crispy-bootstrap5# settings.py
INSTALLED_APPS = [
# ...
'crispy_forms',
'crispy_bootstrap5',
]
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
CRISPY_TEMPLATE_PACK = "bootstrap5"# forms.py
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit, Layout, Fieldset, Div, HTML
class ContactForm(forms.Form):
name = forms.CharField(max_length=100)
email = forms.EmailField()
message = forms.CharField(widget=forms.Textarea())
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.form_action = 'contact'
self.helper.form_class = 'contact-form'
self.helper.label_class = 'form-label'
self.helper.field_class = 'mb-3'
self.helper.layout = Layout(
Fieldset(
'Contact Information',
'name',
'email',
css_class='fieldset-contact'
),
Fieldset(
'Message',
'message',
css_class='fieldset-message'
),
Div(
HTML('<p>We will respond within 24 hours.</p>'),
css_class='info-text'
),
Submit('submit', 'Send Message', css_class='btn btn-primary')
)<!-- Шаблон -->
{% load crispy_forms_tags %}
<form method="post">
{% csrf_token %}
{{ form|crispy }}
</form>
<!-- Или с использованием helper -->
{% load crispy_forms_tags %}
<form method="post" {% if form.is_multipart %}enctype="multipart/form-data"{% endif %}>
{% csrf_token %}
{{ form.helper }}
</form># accounts/forms.py
from django import forms
from django.contrib.auth.models import User
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from django.core.exceptions import ValidationError
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit, Layout, Row, Column
class RegistrationForm(UserCreationForm):
email = forms.EmailField(required=True)
class Meta:
model = User
fields = ['username', 'email', 'password1', 'password2']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.form_action = 'register'
self.helper.layout = Layout(
Row(
Column('username', css_class='col-md-6'),
Column('email', css_class='col-md-6'),
),
Row(
Column('password1', css_class='col-md-6'),
Column('password2', css_class='col-md-6'),
),
Submit('submit', 'Register', css_class='btn btn-primary btn-block')
)
def clean_email(self):
email = self.cleaned_data.get('email')
if User.objects.filter(email=email).exists():
raise ValidationError('Email already registered')
return email
class LoginForm(AuthenticationForm):
username = forms.CharField(
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Username'
})
)
password = forms.CharField(
widget=forms.PasswordInput(attrs={
'class': 'form-control',
'placeholder': 'Password'
})
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.form_action = 'login'
self.helper.layout = Layout(
'username',
'password',
Submit('submit', 'Login', css_class='btn btn-primary btn-block')
)
class ProfileForm(forms.ModelForm):
class Meta:
model = User
fields = ['first_name', 'last_name', 'email']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.layout = Layout(
Row(
Column('first_name', css_class='col-md-6'),
Column('last_name', css_class='col-md-6'),
),
'email',
Submit('submit', 'Update Profile', css_class='btn btn-primary')
)# accounts/views.py
from django.shortcuts import render, redirect
from django.contrib.auth import login, logout
from django.contrib.auth.decorators import login_required
from .forms import RegistrationForm, LoginForm, ProfileForm
def register(request):
if request.method == 'POST':
form = RegistrationForm(request.POST)
if form.is_valid():
user = form.save()
login(request, user)
return redirect('profile')
else:
form = RegistrationForm()
return render(request, 'accounts/register.html', {'form': form})
def login_view(request):
if request.method == 'POST':
form = LoginForm(request, data=request.POST)
if form.is_valid():
user = form.get_user()
login(request, user)
return redirect('profile')
else:
form = LoginForm()
return render(request, 'accounts/login.html', {'form': form})
@login_required
def profile(request):
if request.method == 'POST':
form = ProfileForm(request.POST, instance=request.user)
if form.is_valid():
form.save()
return redirect('profile')
else:
form = ProfileForm(instance=request.user)
return render(request, 'accounts/profile.html', {'form': form})
@login_required
def logout_view(request):
logout(request)
return redirect('login')Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.