← Назад к вопросам

Как реализовать генерацию и рендеринг форм на основе модели, аналогично Form-компоненту в Django?

3.0 Senior🔥 91 комментариев
#Django#Архитектура и паттерны

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Генерация и рендеринг форм на основе модели

Аналогично Django Forms, можно создать систему для автоматического генерирования и рендеринга форм на основе моделей данных. Рассмотрю несколько подходов.

1. Вариант 1: Pydantic + HTMLGen (простой)

Используем Pydantic для определения моделей и генерируем HTML автоматически:

from pydantic import BaseModel, Field, EmailStr, field_validator
from typing import Optional, Type
from html import escape

class FormField:
    """Мета-информация для формы"""
    def __init__(self, field_type, label=None, required=True, 
                 placeholder=None, help_text=None, choices=None):
        self.field_type = field_type
        self.label = label
        self.required = required
        self.placeholder = placeholder
        self.help_text = help_text
        self.choices = choices

class UserRegistration(BaseModel):
    username: str = Field(..., min_length=3, max_length=50)
    email: EmailStr
    password: str = Field(..., min_length=8)
    confirm_password: str
    age: Optional[int] = Field(None, ge=18, le=120)
    country: str = Field(default='US')
    subscribe: bool = Field(default=False)
    
    @field_validator('confirm_password')
    def passwords_match(cls, v, info):
        if v != info.data.get('password'):
            raise ValueError('Пароли не совпадают')
        return v

class FormRenderer:
    """Генерирует HTML форму из Pydantic модели"""
    
    FIELD_TYPES = {
        str: 'text',
        int: 'number',
        bool: 'checkbox',
        EmailStr: 'email',
        float: 'number',
    }
    
    def __init__(self, model: Type[BaseModel]):
        self.model = model
        self.fields = model.model_fields
    
    def render_field(self, field_name: str, field_info) -> str:
        """Генерирует HTML для одного поля"""
        field_type = self.FIELD_TYPES.get(
            field_info.annotation, 'text'
        )
        required = field_info.is_required()
        label = field_info.title or field_name.replace('_', ' ').title()
        
        if field_type == 'checkbox':
            return f"""
            <div class="form-group">
                <label>
                    <input type="checkbox" name="{field_name}" />
                    {escape(label)}
                </label>
            </div>
            """
        else:
            required_attr = 'required' if required else ''
            return f"""
            <div class="form-group">
                <label for="{field_name}">{escape(label)}</label>
                <input 
                    type="{field_type}" 
                    id="{field_name}" 
                    name="{field_name}"
                    {required_attr}
                />
            </div>
            """
    
    def render(self, action: str = '/submit', method: str = 'POST') -> str:
        """Генерирует полную форму"""
        fields_html = ''.join(
            self.render_field(name, field)
            for name, field in self.fields.items()
        )
        
        return f"""
        <form action="{action}" method="{method}" class="form">
            {fields_html}
            <button type="submit" class="btn btn-primary">Submit</button>
        </form>
        """

# Использование
renderer = FormRenderer(UserRegistration)
html = renderer.render('/api/register')
print(html)

2. Вариант 2: Классический Django-подобный подход

Создаём систему похожую на Django Forms:

from typing import Any, Dict, List, Type
from dataclasses import dataclass, fields as dc_fields
from abc import ABC, abstractmethod

@dataclass
class Field(ABC):
    """Базовое поле формы"""
    name: str
    label: str = None
    required: bool = True
    help_text: str = None
    default: Any = None
    
    def __post_init__(self):
        if self.label is None:
            self.label = self.name.replace('_', ' ').title()
    
    @abstractmethod
    def render(self) -> str:
        pass
    
    @abstractmethod
    def validate(self, value: Any) -> Any:
        pass

@dataclass
class StringField(Field):
    min_length: int = 0
    max_length: int = 255
    placeholder: str = None
    
    def render(self) -> str:
        required = 'required' if self.required else ''
        placeholder = f'placeholder="{self.placeholder}"' if self.placeholder else ''
        return f"""
        <div class="form-group">
            <label for="{self.name}">{self.label}</label>
            <input 
                type="text" 
                id="{self.name}" 
                name="{self.name}"
                minlength="{self.min_length}"
                maxlength="{self.max_length}"
                {placeholder}
                {required}
            />
            {f'<small>{self.help_text}</small>' if self.help_text else ''}
        </div>
        """
    
    def validate(self, value: str) -> str:
        if not value and self.required:
            raise ValueError(f'{self.label} обязательно')
        if value and (len(value) < self.min_length or len(value) > self.max_length):
            raise ValueError(f'{self.label} должно быть от {self.min_length} до {self.max_length} символов')
        return value

@dataclass
class IntegerField(Field):
    min_value: int = None
    max_value: int = None
    
    def render(self) -> str:
        required = 'required' if self.required else ''
        min_attr = f'min="{self.min_value}"' if self.min_value is not None else ''
        max_attr = f'max="{self.max_value}"' if self.max_value is not None else ''
        return f"""
        <div class="form-group">
            <label for="{self.name}">{self.label}</label>
            <input 
                type="number" 
                id="{self.name}" 
                name="{self.name}"
                {min_attr}
                {max_attr}
                {required}
            />
        </div>
        """
    
    def validate(self, value: int) -> int:
        if value is None and self.required:
            raise ValueError(f'{self.label} обязательно')
        if value is not None:
            if self.min_value is not None and value < self.min_value:
                raise ValueError(f'{self.label} не может быть меньше {self.min_value}')
            if self.max_value is not None and value > self.max_value:
                raise ValueError(f'{self.label} не может быть больше {self.max_value}')
        return value

@dataclass
class BooleanField(Field):
    def render(self) -> str:
        checked = 'checked' if self.default else ''
        return f"""
        <div class="form-group">
            <label>
                <input 
                    type="checkbox" 
                    name="{self.name}"
                    {checked}
                />
                {self.label}
            </label>
        </div>
        """
    
    def validate(self, value: bool) -> bool:
        return bool(value)

class Form:
    """Контейнер для полей формы"""
    
    def __init__(self):
        self.fields: Dict[str, Field] = {}
        self.errors: Dict[str, List[str]] = {}
    
    def add_field(self, field: Field) -> None:
        self.fields[field.name] = field
    
    def render(self, action: str = '/submit', method: str = 'POST') -> str:
        fields_html = '\n'.join(
            field.render() for field in self.fields.values()
        )
        return f"""
        <form action="{action}" method="{method}" class="form">
            {fields_html}
            <button type="submit" class="btn btn-primary">Submit</button>
        </form>
        """
    
    def validate(self, data: Dict[str, Any]) -> bool:
        """Валидирует все поля"""
        self.errors.clear()
        
        for field_name, field in self.fields.items():
            try:
                value = data.get(field_name)
                field.validate(value)
            except ValueError as e:
                if field_name not in self.errors:
                    self.errors[field_name] = []
                self.errors[field_name].append(str(e))
        
        return len(self.errors) == 0

# Использование
class UserForm(Form):
    def __init__(self):
        super().__init__()
        self.add_field(StringField(
            'username',
            label='Username',
            min_length=3,
            max_length=50,
            placeholder='Enter username'
        ))
        self.add_field(StringField(
            'email',
            label='Email',
            placeholder='user@example.com'
        ))
        self.add_field(IntegerField(
            'age',
            label='Age',
            min_value=18,
            max_value=120,
            required=False
        ))
        self.add_field(BooleanField(
            'subscribe',
            label='Subscribe to newsletter',
            default=False,
            required=False
        ))

form = UserForm()
print(form.render('/api/register'))

# Валидация
data = {'username': 'john', 'email': 'john@example.com', 'age': 25, 'subscribe': True}
if form.validate(data):
    print('Форма валидна')
else:
    print('Ошибки:', form.errors)

3. Вариант 3: FastAPI + SQLAlchemy (production)

Для реальных приложений используй готовые инструменты:

from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from sqlalchemy import Column, String, Integer, Boolean, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import Session
from pydantic import BaseModel, EmailStr, validator

app = FastAPI()
Base = declarative_base()

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    username = Column(String(50), unique=True)
    email = Column(String(255), unique=True)
    age = Column(Integer)
    is_active = Column(Boolean, default=True)

class UserSchema(BaseModel):
    username: str
    email: EmailStr
    age: int
    is_active: bool = True
    
    class Config:
        from_attributes = True

# Автоматическое преобразование модели в HTML форму
def generate_form_html(schema_class, action: str) -> str:
    """Генерирует HTML форму из Pydantic модели"""
    fields_html = []
    
    for field_name, field_info in schema_class.model_fields.items():
        field_type = field_info.annotation
        required = field_info.is_required()
        
        if field_type == bool:
            fields_html.append(f"""
            <label>
                <input type="checkbox" name="{field_name}" />
                {field_name.title()}
            </label>
            """)
        elif field_type == int:
            fields_html.append(f"""
            <div>
                <label for="{field_name}">{field_name.title()}</label>
                <input type="number" id="{field_name}" name="{field_name}" {'required' if required else ''} />
            </div>
            """)
        else:  # str, EmailStr
            input_type = 'email' if field_type == EmailStr else 'text'
            fields_html.append(f"""
            <div>
                <label for="{field_name}">{field_name.title()}</label>
                <input type="{input_type}" id="{field_name}" name="{field_name}" {'required' if required else ''} />
            </div>
            """)
    
    return f"""
    <html>
    <body>
        <form action="{action}" method="POST">
            {''.join(fields_html)}
            <button type="submit">Submit</button>
        </form>
    </body>
    </html>
    """

@app.get('/register', response_class=HTMLResponse)
async def register_form():
    return generate_form_html(UserSchema, '/api/users')

@app.post('/api/users')
async def create_user(user: UserSchema):
    return {'message': 'User created', 'user': user}

4. Использование готовых библиотек

В продакшене используй проверенные решения:

# WTForms — классический выбор
pip install wtforms

# FastAPI + SQLAdmin — автоматический админ-панель
pip install sqladmin

# Pydantic validation
pip install pydantic
from wtforms import Form, StringField, IntegerField, BooleanField, validators
from wtforms.validators import DataRequired, Email, Length

class UserForm(Form):
    username = StringField('Username', [
        validators.DataRequired(),
        validators.Length(min=3, max=50)
    ])
    email = StringField('Email', [
        validators.DataRequired(),
        validators.Email()
    ])
    age = IntegerField('Age', [
        validators.NumberRange(min=18, max=120)
    ])
    subscribe = BooleanField('Subscribe')

Резюме

Для генерации форм:

  1. Простой вариант — Pydantic + HTMLGen (для прототипов)
  2. Средний — собственная система Field + Form (для контроля)
  3. Production — FastAPI + SQLAlchemy + SQLAdmin или WTForms
  4. Фронтенд-генерация — JSON schema → React/Vue компоненты