first
This commit is contained in:
10
app/repositories/__init__.py
Normal file
10
app/repositories/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from app.repositories.base import BaseRepository
|
||||
from app.repositories.form_repository import FormRepository, FieldRepository
|
||||
from app.repositories.submission_repository import SubmissionRepository
|
||||
|
||||
__all__ = [
|
||||
"BaseRepository",
|
||||
"FormRepository",
|
||||
"FieldRepository",
|
||||
"SubmissionRepository",
|
||||
]
|
||||
243
app/repositories/base.py
Normal file
243
app/repositories/base.py
Normal file
@@ -0,0 +1,243 @@
|
||||
from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func, and_, or_, desc, asc
|
||||
from app.models.base import BaseModel
|
||||
|
||||
# Тип для модели SQLAlchemy
|
||||
ModelType = TypeVar("ModelType", bound=BaseModel)
|
||||
|
||||
|
||||
class BaseRepository(Generic[ModelType]):
|
||||
"""
|
||||
Базовый репозиторий с CRUD операциями.
|
||||
Использует Generic для поддержки разных моделей.
|
||||
"""
|
||||
|
||||
def __init__(self, db: Session, model: Type[ModelType]):
|
||||
"""
|
||||
Инициализация репозитория
|
||||
|
||||
Args:
|
||||
db: Сессия SQLAlchemy
|
||||
model: Класс модели
|
||||
"""
|
||||
self.db = db
|
||||
self.model = model
|
||||
|
||||
def get_by_id(self, id: int) -> Optional[ModelType]:
|
||||
"""Получить запись по ID"""
|
||||
return self.db.query(self.model).filter(self.model.id == id).first()
|
||||
|
||||
def get_by_uuid(self, uuid: str) -> Optional[ModelType]:
|
||||
"""Получить запись по UUID (если модель имеет поле uuid)"""
|
||||
if hasattr(self.model, 'uuid'):
|
||||
return self.db.query(self.model).filter(self.model.uuid == uuid).first()
|
||||
raise AttributeError(f"Model {self.model.__name__} does not have 'uuid' field")
|
||||
|
||||
def get_all(
|
||||
self,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
order_by: Optional[str] = None,
|
||||
descending: bool = False
|
||||
) -> List[ModelType]:
|
||||
"""Получить все записи с пагинацией"""
|
||||
query = self.db.query(self.model)
|
||||
|
||||
if order_by and hasattr(self.model, order_by):
|
||||
order_column = getattr(self.model, order_by)
|
||||
if descending:
|
||||
query = query.order_by(desc(order_column))
|
||||
else:
|
||||
query = query.order_by(asc(order_column))
|
||||
elif hasattr(self.model, 'created_at'):
|
||||
query = query.order_by(desc(self.model.created_at))
|
||||
|
||||
return query.offset(skip).limit(limit).all()
|
||||
|
||||
def create(self, obj_in: Dict[str, Any]) -> ModelType:
|
||||
"""Создать новую запись"""
|
||||
db_obj = self.model(**obj_in)
|
||||
self.db.add(db_obj)
|
||||
self.db.commit()
|
||||
self.db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
def create_many(self, objects_in: List[Dict[str, Any]]) -> List[ModelType]:
|
||||
"""Создать несколько записей"""
|
||||
db_objects = [self.model(**obj_in) for obj_in in objects_in]
|
||||
self.db.add_all(db_objects)
|
||||
self.db.commit()
|
||||
for obj in db_objects:
|
||||
self.db.refresh(obj)
|
||||
return db_objects
|
||||
|
||||
def update(self, id: int, obj_in: Dict[str, Any]) -> Optional[ModelType]:
|
||||
"""Обновить запись"""
|
||||
db_obj = self.get_by_id(id)
|
||||
if not db_obj:
|
||||
return None
|
||||
|
||||
for field, value in obj_in.items():
|
||||
if hasattr(db_obj, field):
|
||||
setattr(db_obj, field, value)
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
def update_by_uuid(self, uuid: str, obj_in: Dict[str, Any]) -> Optional[ModelType]:
|
||||
"""Обновить запись по UUID"""
|
||||
db_obj = self.get_by_uuid(uuid)
|
||||
if not db_obj:
|
||||
return None
|
||||
|
||||
for field, value in obj_in.items():
|
||||
if hasattr(db_obj, field):
|
||||
setattr(db_obj, field, value)
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
def delete(self, id: int) -> bool:
|
||||
"""Удалить запись"""
|
||||
db_obj = self.get_by_id(id)
|
||||
if not db_obj:
|
||||
return False
|
||||
|
||||
self.db.delete(db_obj)
|
||||
self.db.commit()
|
||||
return True
|
||||
|
||||
def delete_by_uuid(self, uuid: str) -> bool:
|
||||
"""Удалить запись по UUID"""
|
||||
db_obj = self.get_by_uuid(uuid)
|
||||
if not db_obj:
|
||||
return False
|
||||
|
||||
self.db.delete(db_obj)
|
||||
self.db.commit()
|
||||
return True
|
||||
|
||||
def count(self, **filters) -> int:
|
||||
"""Подсчитать количество записей с фильтрами"""
|
||||
query = self.db.query(self.model)
|
||||
|
||||
for field, value in filters.items():
|
||||
if hasattr(self.model, field):
|
||||
query = query.filter(getattr(self.model, field) == value)
|
||||
|
||||
return query.count()
|
||||
|
||||
def exists(self, **filters) -> bool:
|
||||
"""Проверить существование записи"""
|
||||
return self.count(**filters) > 0
|
||||
|
||||
def filter_by(
|
||||
self,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
order_by: Optional[str] = None,
|
||||
descending: bool = False,
|
||||
**filters
|
||||
) -> List[ModelType]:
|
||||
"""Фильтрация записей по параметрам"""
|
||||
query = self.db.query(self.model)
|
||||
|
||||
for field, value in filters.items():
|
||||
if hasattr(self.model, field):
|
||||
query = query.filter(getattr(self.model, field) == value)
|
||||
|
||||
if order_by and hasattr(self.model, order_by):
|
||||
order_column = getattr(self.model, order_by)
|
||||
if descending:
|
||||
query = query.order_by(desc(order_column))
|
||||
else:
|
||||
query = query.order_by(asc(order_column))
|
||||
|
||||
return query.offset(skip).limit(limit).all()
|
||||
|
||||
def find_first(self, **filters) -> Optional[ModelType]:
|
||||
"""Найти первую запись по фильтрам"""
|
||||
query = self.db.query(self.model)
|
||||
|
||||
for field, value in filters.items():
|
||||
if hasattr(self.model, field):
|
||||
query = query.filter(getattr(self.model, field) == value)
|
||||
|
||||
return query.first()
|
||||
|
||||
def bulk_update(self, ids: List[int], obj_in: Dict[str, Any]) -> int:
|
||||
"""Массовое обновление записей"""
|
||||
updated_count = self.db.query(self.model).filter(
|
||||
self.model.id.in_(ids)
|
||||
).update(obj_in, synchronize_session=False)
|
||||
|
||||
self.db.commit()
|
||||
return updated_count
|
||||
|
||||
def bulk_delete(self, ids: List[int]) -> int:
|
||||
"""Массовое удаление записей"""
|
||||
deleted_count = self.db.query(self.model).filter(
|
||||
self.model.id.in_(ids)
|
||||
).delete(synchronize_session=False)
|
||||
|
||||
self.db.commit()
|
||||
return deleted_count
|
||||
|
||||
def get_or_create(self, defaults: Optional[Dict] = None, **filters) -> tuple[ModelType, bool]:
|
||||
"""
|
||||
Получить запись или создать новую
|
||||
|
||||
Returns:
|
||||
tuple: (объект, создан_ли_новый)
|
||||
"""
|
||||
instance = self.find_first(**filters)
|
||||
|
||||
if instance:
|
||||
return instance, False
|
||||
|
||||
if defaults:
|
||||
filters.update(defaults)
|
||||
|
||||
return self.create(filters), True
|
||||
|
||||
def paginate(
|
||||
self,
|
||||
page: int = 1,
|
||||
per_page: int = 20,
|
||||
order_by: Optional[str] = None,
|
||||
descending: bool = False,
|
||||
**filters
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Пагинированный список записей
|
||||
|
||||
Returns:
|
||||
Dict с ключами: items, total, page, per_page, pages
|
||||
"""
|
||||
query = self.db.query(self.model)
|
||||
|
||||
for field, value in filters.items():
|
||||
if hasattr(self.model, field):
|
||||
query = query.filter(getattr(self.model, field) == value)
|
||||
|
||||
total = query.count()
|
||||
|
||||
if order_by and hasattr(self.model, order_by):
|
||||
order_column = getattr(self.model, order_by)
|
||||
if descending:
|
||||
query = query.order_by(desc(order_column))
|
||||
else:
|
||||
query = query.order_by(asc(order_column))
|
||||
|
||||
items = query.offset((page - 1) * per_page).limit(per_page).all()
|
||||
|
||||
return {
|
||||
"items": items,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"per_page": per_page,
|
||||
"pages": (total + per_page - 1) // per_page
|
||||
}
|
||||
98
app/repositories/form_repository.py
Normal file
98
app/repositories/form_repository.py
Normal file
@@ -0,0 +1,98 @@
|
||||
from typing import Optional, List, Dict, Any
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, or_
|
||||
from app.repositories.base import BaseRepository
|
||||
from app.models.form import Form, FormField, Field
|
||||
|
||||
|
||||
class FormRepository(BaseRepository[Form]):
|
||||
def __init__(self, db: Session):
|
||||
super().__init__(db, Form)
|
||||
|
||||
def get_by_name(self, name: str) -> Optional[Form]:
|
||||
"""Получить форму по имени"""
|
||||
return self.db.query(Form).filter(Form.name == name).first()
|
||||
|
||||
def get_active_forms(self, skip: int = 0, limit: int = 100) -> List[Form]:
|
||||
"""Получить активные формы"""
|
||||
return self.db.query(Form).filter(
|
||||
Form.is_active == True,
|
||||
Form.is_published == True
|
||||
).offset(skip).limit(limit).all()
|
||||
|
||||
def get_forms_with_submissions_count(self) -> List[Dict[str, Any]]:
|
||||
"""Получить формы с количеством submissions"""
|
||||
from app.models.submission import Submission
|
||||
|
||||
results = self.db.query(
|
||||
Form.id,
|
||||
Form.name,
|
||||
Form.description,
|
||||
Form.created_at,
|
||||
func.count(Submission.id).label('submissions_count')
|
||||
).outerjoin(
|
||||
Submission, Form.id == Submission.form_id
|
||||
).group_by(
|
||||
Form.id
|
||||
).all()
|
||||
|
||||
return [
|
||||
{
|
||||
"id": r.id,
|
||||
"name": r.name,
|
||||
"description": r.description,
|
||||
"created_at": r.created_at,
|
||||
"submissions_count": r.submissions_count
|
||||
}
|
||||
for r in results
|
||||
]
|
||||
|
||||
def clone_form(self, form_id: int, new_name: str) -> Optional[Form]:
|
||||
"""Клонировать форму со всеми полями"""
|
||||
original_form = self.get_by_id(form_id)
|
||||
if not original_form:
|
||||
return None
|
||||
|
||||
# Клонируем форму
|
||||
cloned_form = Form(
|
||||
name=new_name,
|
||||
description=original_form.description,
|
||||
settings=original_form.settings,
|
||||
is_active=False,
|
||||
is_published=False
|
||||
)
|
||||
self.db.add(cloned_form)
|
||||
self.db.flush()
|
||||
|
||||
# Клонируем связи с полями
|
||||
for form_field in original_form.fields:
|
||||
new_form_field = FormField(
|
||||
form_id=cloned_form.id,
|
||||
field_id=form_field.field_id,
|
||||
order=form_field.order,
|
||||
is_required=form_field.is_required,
|
||||
default_value=form_field.default_value,
|
||||
visibility_conditions=form_field.visibility_conditions,
|
||||
validation_rules_override=form_field.validation_rules_override
|
||||
)
|
||||
self.db.add(new_form_field)
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(cloned_form)
|
||||
return cloned_form
|
||||
|
||||
|
||||
class FieldRepository(BaseRepository[Field]):
|
||||
def __init__(self, db: Session):
|
||||
super().__init__(db, Field)
|
||||
|
||||
def get_by_name(self, name: str) -> Optional[Field]:
|
||||
"""Получить поле по имени"""
|
||||
return self.db.query(Field).filter(Field.name == name).first()
|
||||
|
||||
def get_or_create_field(self, field_data: Dict[str, Any]) -> Field:
|
||||
"""Получить или создать поле"""
|
||||
field = self.get_by_name(field_data.get("name"))
|
||||
if not field:
|
||||
field = self.create(field_data)
|
||||
return field
|
||||
105
app/repositories/submission_repository.py
Normal file
105
app/repositories/submission_repository.py
Normal file
@@ -0,0 +1,105 @@
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func, and_, or_, text
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime, timedelta
|
||||
from app.models.submission import Submission
|
||||
from app.repositories.base import BaseRepository
|
||||
|
||||
|
||||
class SubmissionRepository(BaseRepository):
|
||||
def __init__(self, db: Session):
|
||||
super().__init__(db, Submission)
|
||||
|
||||
def filter_by_field_value(self, form_id: int, field_name: str, value: Any):
|
||||
"""Фильтрация по значению поля в JSONB"""
|
||||
return self.db.query(Submission).filter(
|
||||
Submission.form_id == form_id,
|
||||
Submission.data[field_name].astext == str(value)
|
||||
)
|
||||
|
||||
def filter_by_multiple_fields(self, form_id: int, filters: Dict[str, Any]):
|
||||
"""Фильтрация по нескольким полям"""
|
||||
query = self.db.query(Submission).filter(Submission.form_id == form_id)
|
||||
|
||||
for field_name, value in filters.items():
|
||||
# Используем JSONB оператор @> для точного совпадения
|
||||
query = query.filter(
|
||||
Submission.data[field_name].astext == str(value)
|
||||
)
|
||||
|
||||
return query
|
||||
|
||||
def search_in_jsonb(self, form_id: int, search_text: str, fields: Optional[List[str]] = None):
|
||||
"""Полнотекстовый поиск по JSONB полям"""
|
||||
if fields:
|
||||
# Поиск только в указанных полях
|
||||
conditions = []
|
||||
for field in fields:
|
||||
conditions.append(
|
||||
Submission.data[field].astext.ilike(f'%{search_text}%')
|
||||
)
|
||||
return self.db.query(Submission).filter(
|
||||
Submission.form_id == form_id,
|
||||
or_(*conditions)
|
||||
)
|
||||
else:
|
||||
# Поиск по всем полям через преобразование JSONB в текст
|
||||
return self.db.query(Submission).filter(
|
||||
Submission.form_id == form_id,
|
||||
func.cast(Submission.data, String).ilike(f'%{search_text}%')
|
||||
)
|
||||
|
||||
def get_field_statistics(self, form_id: int, field_name: str):
|
||||
"""Статистика по полю"""
|
||||
from sqlalchemy import case
|
||||
|
||||
results = self.db.query(
|
||||
Submission.data[field_name].astext.label('value'),
|
||||
func.count(Submission.id).label('count')
|
||||
).filter(
|
||||
Submission.form_id == form_id,
|
||||
Submission.data[field_name].isnot(None)
|
||||
).group_by(
|
||||
Submission.data[field_name].astext
|
||||
).all()
|
||||
|
||||
return {row.value: row.count for row in results}
|
||||
|
||||
def get_daily_submissions(self, form_id: int, days: int = 30):
|
||||
"""Статистика по дням"""
|
||||
start_date = datetime.utcnow() - timedelta(days=days)
|
||||
|
||||
results = self.db.query(
|
||||
func.date(Submission.submitted_at).label('date'),
|
||||
func.count(Submission.id).label('count')
|
||||
).filter(
|
||||
Submission.form_id == form_id,
|
||||
Submission.submitted_at >= start_date
|
||||
).group_by(
|
||||
func.date(Submission.submitted_at)
|
||||
).order_by(
|
||||
'date'
|
||||
).all()
|
||||
|
||||
return [{"date": r.date, "count": r.count} for r in results]
|
||||
|
||||
def get_advanced_analytics(self, form_id: int):
|
||||
"""Расширенная аналитика с использованием JSONB функций PostgreSQL"""
|
||||
|
||||
# Количество уникальных значений для каждого поля
|
||||
query = text("""
|
||||
SELECT
|
||||
COUNT(*) as total_submissions,
|
||||
COUNT(DISTINCT data->>'email') as unique_emails,
|
||||
AVG((data->>'age')::int) as average_age,
|
||||
jsonb_object_agg(
|
||||
data->>'country',
|
||||
COUNT(*)
|
||||
) as country_distribution
|
||||
FROM submissions
|
||||
WHERE form_id = :form_id
|
||||
GROUP BY form_id
|
||||
""")
|
||||
|
||||
result = self.db.execute(query, {"form_id": form_id}).first()
|
||||
return dict(result._mapping) if result else {}
|
||||
Reference in New Issue
Block a user