first
This commit is contained in:
92
app/schemas/__init__.py
Normal file
92
app/schemas/__init__.py
Normal file
@@ -0,0 +1,92 @@
|
||||
# app/schemas/__init__.py
|
||||
# Common
|
||||
from app.schemas.common import (
|
||||
BaseSchema,
|
||||
PaginationParams,
|
||||
PaginatedResponse,
|
||||
MessageResponse,
|
||||
ErrorResponse
|
||||
)
|
||||
|
||||
# Field
|
||||
from app.schemas.field import (
|
||||
FieldType,
|
||||
FieldValidationRules,
|
||||
FieldOptions,
|
||||
FieldCreate,
|
||||
FieldUpdate,
|
||||
FieldResponse
|
||||
)
|
||||
|
||||
# Form
|
||||
from app.schemas.form import (
|
||||
FormSettings,
|
||||
FormCreate,
|
||||
FormUpdate,
|
||||
FormResponse,
|
||||
FormWithFieldsResponse,
|
||||
FormPublishRequest # ✅ теперь импорт работает
|
||||
)
|
||||
|
||||
# Submission
|
||||
from app.schemas.submission import (
|
||||
SubmissionMetadata,
|
||||
SubmissionCreate,
|
||||
SubmissionUpdate,
|
||||
SubmissionResponse,
|
||||
SubmissionWithFormResponse,
|
||||
SubmissionFilterParams
|
||||
)
|
||||
|
||||
# Response
|
||||
from app.schemas.response import (
|
||||
HealthResponse,
|
||||
ValidationErrorDetail,
|
||||
ValidationErrorResponse,
|
||||
ExportResponse,
|
||||
AnalyticsFieldStats,
|
||||
AnalyticsDailyStats,
|
||||
FormAnalyticsResponse
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Common
|
||||
"BaseSchema",
|
||||
"PaginationParams",
|
||||
"PaginatedResponse",
|
||||
"MessageResponse",
|
||||
"ErrorResponse",
|
||||
|
||||
# Field
|
||||
"FieldType",
|
||||
"FieldValidationRules",
|
||||
"FieldOptions",
|
||||
"FieldCreate",
|
||||
"FieldUpdate",
|
||||
"FieldResponse",
|
||||
|
||||
# Form
|
||||
"FormSettings",
|
||||
"FormCreate",
|
||||
"FormUpdate",
|
||||
"FormResponse",
|
||||
"FormWithFieldsResponse",
|
||||
"FormPublishRequest",
|
||||
|
||||
# Submission
|
||||
"SubmissionMetadata",
|
||||
"SubmissionCreate",
|
||||
"SubmissionUpdate",
|
||||
"SubmissionResponse",
|
||||
"SubmissionWithFormResponse",
|
||||
"SubmissionFilterParams",
|
||||
|
||||
# Response
|
||||
"HealthResponse",
|
||||
"ValidationErrorDetail",
|
||||
"ValidationErrorResponse",
|
||||
"ExportResponse",
|
||||
"AnalyticsFieldStats",
|
||||
"AnalyticsDailyStats",
|
||||
"FormAnalyticsResponse",
|
||||
]
|
||||
45
app/schemas/common.py
Normal file
45
app/schemas/common.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, Generic, TypeVar, List
|
||||
from datetime import datetime
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
class BaseSchema(BaseModel):
|
||||
"""Базовый класс для всех схем"""
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
json_encoders = {
|
||||
datetime: lambda v: v.isoformat()
|
||||
}
|
||||
|
||||
|
||||
class PaginationParams(BaseModel):
|
||||
"""Параметры пагинации"""
|
||||
page: int = Field(1, ge=1, description="Номер страницы")
|
||||
per_page: int = Field(20, ge=1, le=100, description="Записей на странице")
|
||||
order_by: Optional[str] = Field(None, description="Поле для сортировки")
|
||||
descending: bool = Field(True, description="По убыванию")
|
||||
|
||||
|
||||
class PaginatedResponse(BaseSchema, Generic[T]):
|
||||
"""Ответ с пагинацией"""
|
||||
items: List[T]
|
||||
total: int
|
||||
page: int
|
||||
per_page: int
|
||||
pages: int
|
||||
|
||||
|
||||
class MessageResponse(BaseSchema):
|
||||
"""Простой ответ с сообщением"""
|
||||
message: str
|
||||
success: bool = True
|
||||
|
||||
|
||||
class ErrorResponse(BaseSchema):
|
||||
"""Ответ с ошибкой"""
|
||||
error: str
|
||||
detail: Optional[str] = None
|
||||
status_code: int
|
||||
102
app/schemas/field.py
Normal file
102
app/schemas/field.py
Normal file
@@ -0,0 +1,102 @@
|
||||
# app/schemas/field.py
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, Field, validator
|
||||
from typing import Optional, List, Dict, Any
|
||||
from enum import Enum
|
||||
from app.schemas.common import BaseSchema
|
||||
|
||||
|
||||
class FieldType(str, Enum):
|
||||
"""Типы полей формы"""
|
||||
TEXT = "text"
|
||||
TEXTAREA = "textarea"
|
||||
NUMBER = "number"
|
||||
EMAIL = "email"
|
||||
PHONE = "phone"
|
||||
DATE = "date"
|
||||
DATETIME = "datetime"
|
||||
TIME = "time"
|
||||
SELECT = "select"
|
||||
MULTISELECT = "multiselect"
|
||||
RADIO = "radio"
|
||||
CHECKBOX = "checkbox"
|
||||
FILE = "file"
|
||||
IMAGE = "image"
|
||||
URL = "url"
|
||||
COLOR = "color"
|
||||
RANGE = "range"
|
||||
HIDDEN = "hidden"
|
||||
|
||||
|
||||
class FieldValidationRules(BaseModel):
|
||||
"""Правила валидации поля"""
|
||||
min_length: Optional[int] = Field(None, ge=0)
|
||||
max_length: Optional[int] = Field(None, ge=0)
|
||||
min_value: Optional[float] = None
|
||||
max_value: Optional[float] = None
|
||||
pattern: Optional[str] = None # regex pattern
|
||||
required: bool = False
|
||||
custom_error_message: Optional[str] = None
|
||||
|
||||
|
||||
class FieldOptions(BaseModel):
|
||||
"""Опции для полей типа select/radio/checkbox"""
|
||||
options: List[Dict[str, Any]] = Field(default_factory=list)
|
||||
multiple: bool = False
|
||||
searchable: bool = False
|
||||
placeholder: Optional[str] = None
|
||||
|
||||
|
||||
class FieldCreate(BaseSchema):
|
||||
"""Создание поля"""
|
||||
name: str = Field(..., min_length=1, max_length=100,
|
||||
pattern=r'^[a-zA-Z_][a-zA-Z0-9_]*$') # ✅ заменено regex на pattern
|
||||
label: str = Field(..., min_length=1, max_length=200)
|
||||
field_type: FieldType
|
||||
placeholder: Optional[str] = Field(None, max_length=200)
|
||||
help_text: Optional[str] = None
|
||||
default_value: Optional[Any] = None
|
||||
options: Optional[FieldOptions] = None
|
||||
validation_rules: Optional[FieldValidationRules] = None
|
||||
metadata: Dict[str, Any] = Field(default_factory=dict)
|
||||
order: int = Field(0, ge=0)
|
||||
is_required: bool = False
|
||||
|
||||
@validator('name')
|
||||
def validate_name(cls, v):
|
||||
if v in ['id', 'created_at', 'updated_at', 'form_id', 'submission_id']:
|
||||
raise ValueError(f'Field name "{v}" is reserved')
|
||||
return v
|
||||
|
||||
|
||||
class FieldUpdate(BaseSchema):
|
||||
"""Обновление поля"""
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=100,
|
||||
pattern=r'^[a-zA-Z_][a-zA-Z0-9_]*$') # ✅ pattern вместо regex
|
||||
label: Optional[str] = Field(None, min_length=1, max_length=200)
|
||||
field_type: Optional[FieldType] = None
|
||||
placeholder: Optional[str] = None
|
||||
help_text: Optional[str] = None
|
||||
default_value: Optional[Any] = None
|
||||
options: Optional[FieldOptions] = None
|
||||
validation_rules: Optional[FieldValidationRules] = None
|
||||
metadata: Optional[Dict[str, Any]] = None
|
||||
order: Optional[int] = Field(None, ge=0)
|
||||
is_required: Optional[bool] = None
|
||||
|
||||
|
||||
class FieldResponse(BaseSchema):
|
||||
id: int
|
||||
name: str
|
||||
label: str
|
||||
field_type: FieldType
|
||||
placeholder: Optional[str] = None
|
||||
help_text: Optional[str] = None
|
||||
options: Dict[str, Any] = Field(default_factory=dict)
|
||||
validation_rules: Dict[str, Any] = Field(default_factory=dict)
|
||||
metadata: Dict[str, Any] = Field(default_factory=dict)
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
69
app/schemas/form.py
Normal file
69
app/schemas/form.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# app/schemas/form.py
|
||||
from pydantic import BaseModel, Field, validator
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime
|
||||
from app.schemas.common import BaseSchema
|
||||
from app.schemas.field import FieldResponse
|
||||
|
||||
|
||||
class FormSettings(BaseModel):
|
||||
"""Настройки формы"""
|
||||
theme: Optional[str] = "light"
|
||||
show_submit_button: bool = True
|
||||
submit_button_text: str = "Submit"
|
||||
success_message: str = "Form submitted successfully"
|
||||
redirect_url: Optional[str] = None
|
||||
send_notifications: bool = False
|
||||
notification_emails: List[str] = Field(default_factory=list)
|
||||
limit_one_submission_per_user: bool = False
|
||||
enable_captcha: bool = False
|
||||
|
||||
|
||||
class FormCreate(BaseSchema):
|
||||
"""Создание формы"""
|
||||
name: str = Field(..., min_length=1, max_length=200)
|
||||
description: Optional[str] = Field(None, max_length=500)
|
||||
settings: FormSettings = Field(default_factory=FormSettings)
|
||||
fields: List[Dict[str, Any]] = Field(..., min_items=1)
|
||||
|
||||
@validator('name')
|
||||
def validate_name(cls, v):
|
||||
if not v.strip():
|
||||
raise ValueError('Name cannot be empty')
|
||||
return v.strip()
|
||||
|
||||
|
||||
class FormUpdate(BaseSchema):
|
||||
"""Обновление формы"""
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=200)
|
||||
description: Optional[str] = None
|
||||
settings: Optional[FormSettings] = None
|
||||
is_active: Optional[bool] = None
|
||||
is_published: Optional[bool] = None
|
||||
|
||||
|
||||
class FormResponse(BaseSchema):
|
||||
"""Ответ с данными формы"""
|
||||
id: int
|
||||
name: str
|
||||
description: Optional[str]
|
||||
version: int
|
||||
is_active: bool
|
||||
is_published: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
created_by: Optional[int]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class FormWithFieldsResponse(FormResponse):
|
||||
"""Форма со всеми полями"""
|
||||
fields: List[FieldResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
class FormPublishRequest(BaseSchema):
|
||||
"""Публикация формы"""
|
||||
publish: bool = True
|
||||
version_note: Optional[str] = None
|
||||
66
app/schemas/response.py
Normal file
66
app/schemas/response.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, Any, Dict, List, Generic, TypeVar
|
||||
from datetime import datetime
|
||||
from app.schemas.common import BaseSchema, PaginatedResponse
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
class HealthResponse(BaseSchema):
|
||||
"""Health check ответ"""
|
||||
status: str = "healthy"
|
||||
timestamp: datetime = Field(default_factory=datetime.utcnow)
|
||||
version: str = "1.0.0"
|
||||
database: str
|
||||
|
||||
class ValidationErrorDetail(BaseSchema):
|
||||
"""Детали ошибки валидации"""
|
||||
field: str
|
||||
message: str
|
||||
value: Optional[Any] = None
|
||||
|
||||
class ValidationErrorResponse(BaseSchema):
|
||||
"""Ответ с ошибками валидации"""
|
||||
error: str = "Validation Error"
|
||||
details: List[ValidationErrorDetail] = Field(default_factory=list)
|
||||
status_code: int = 422
|
||||
|
||||
class ExportResponse(BaseSchema):
|
||||
"""Ответ на экспорт данных"""
|
||||
download_url: str
|
||||
file_name: str
|
||||
file_size: int
|
||||
expires_at: datetime
|
||||
format: str # csv, xlsx, json
|
||||
|
||||
class AnalyticsFieldStats(BaseSchema):
|
||||
"""Статистика по полю формы"""
|
||||
field_name: str
|
||||
field_label: str
|
||||
field_type: str
|
||||
total_responses: int
|
||||
filled_count: int
|
||||
empty_count: int
|
||||
fill_rate: float # процент заполнения
|
||||
unique_values: int
|
||||
distribution: Dict[str, int] # значение -> количество
|
||||
|
||||
class AnalyticsDailyStats(BaseSchema):
|
||||
"""Ежедневная статистика"""
|
||||
date: str
|
||||
count: int
|
||||
avg_completion_time: Optional[float] = None
|
||||
|
||||
class FormAnalyticsResponse(BaseSchema):
|
||||
"""Аналитика по форме"""
|
||||
form_id: int
|
||||
form_name: str
|
||||
total_submissions: int
|
||||
unique_submitters: int
|
||||
avg_completion_time: Optional[float]
|
||||
completion_rate: float # процент завершенных
|
||||
fields_stats: List[AnalyticsFieldStats]
|
||||
daily_stats: List[AnalyticsDailyStats]
|
||||
peak_hours: Dict[int, int] # час -> количество
|
||||
trend: str # increasing, decreasing, stable
|
||||
last_submission_at: Optional[datetime]
|
||||
first_submission_at: Optional[datetime]
|
||||
71
app/schemas/submission.py
Normal file
71
app/schemas/submission.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# app/schemas/submission.py
|
||||
from pydantic import BaseModel, Field, validator
|
||||
from typing import Optional, Dict, Any, List
|
||||
from datetime import datetime
|
||||
from app.schemas.common import BaseSchema
|
||||
|
||||
|
||||
class SubmissionMetadata(BaseModel):
|
||||
"""Метаданные submission"""
|
||||
ip_address: Optional[str] = None
|
||||
user_agent: Optional[str] = None
|
||||
referrer: Optional[str] = None
|
||||
browser: Optional[str] = None
|
||||
os: Optional[str] = None
|
||||
device: Optional[str] = None
|
||||
location: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class SubmissionCreate(BaseSchema):
|
||||
"""Создание submission (отправка формы)"""
|
||||
form_id: int
|
||||
data: Dict[str, Any] = Field(..., description="Данные формы")
|
||||
metadata: Optional[SubmissionMetadata] = None
|
||||
|
||||
@validator('data')
|
||||
def validate_data_not_empty(cls, v):
|
||||
if not v:
|
||||
raise ValueError('Data cannot be empty')
|
||||
return v
|
||||
|
||||
|
||||
class SubmissionUpdate(BaseSchema):
|
||||
"""Обновление submission"""
|
||||
data: Optional[Dict[str, Any]] = None
|
||||
status: Optional[str] = Field(None, pattern='^(draft|completed|rejected)$')
|
||||
completion_time_seconds: Optional[float] = Field(None, ge=0)
|
||||
|
||||
|
||||
class SubmissionResponse(BaseSchema):
|
||||
"""Ответ с данными submission - соответствует модели БД"""
|
||||
id: int
|
||||
submission_uuid: str
|
||||
form_id: int
|
||||
submission_data: Dict[str, Any] # ← изменено с data на submission_data
|
||||
submission_metadata: Dict[str, Any] = Field(default_factory=dict) # ← изменено с metadata
|
||||
status: str
|
||||
submitted_at: datetime
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
submitted_by: Optional[int]
|
||||
completion_time_seconds: Optional[float]
|
||||
referrer: Optional[str]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class SubmissionWithFormResponse(SubmissionResponse):
|
||||
"""Submission с данными формы"""
|
||||
form_name: Optional[str] = None
|
||||
form_version: Optional[int] = None
|
||||
|
||||
|
||||
class SubmissionFilterParams(BaseSchema):
|
||||
"""Параметры фильтрации submissions"""
|
||||
status: Optional[str] = None
|
||||
date_from: Optional[datetime] = None
|
||||
date_to: Optional[datetime] = None
|
||||
search: Optional[str] = None
|
||||
search_fields: Optional[List[str]] = None
|
||||
field_filters: Dict[str, Any] = Field(default_factory=dict)
|
||||
Reference in New Issue
Block a user