This commit is contained in:
2026-04-09 19:28:41 +03:00
commit 9fa723bb4c
43 changed files with 2804 additions and 0 deletions

12
app/services/__init__.py Normal file
View File

@@ -0,0 +1,12 @@
# app/services/__init__.py
from app.services.form_service import FormService
from app.services.submission_service import SubmissionService
from app.services.analytics_service import AnalyticsService
from app.services.export_service import ExportService
__all__ = [
"FormService",
"SubmissionService",
"AnalyticsService",
"ExportService"
]

View File

@@ -0,0 +1,135 @@
from sqlalchemy.orm import Session
from typing import Dict, Any, List
from app.repositories.submission_repository import SubmissionRepository
from app.repositories.form_repository import FormRepository
class AnalyticsService:
def __init__(self, db: Session):
self.db = db
self.submission_repo = SubmissionRepository(db)
self.form_repo = FormRepository(db)
def get_form_report(self, form_id: int) -> Dict[str, Any]:
"""Полный отчет по форме"""
form = self.form_repo.get_by_id(form_id)
if not form:
raise ValueError("Form not found")
# Базовая статистика
total_submissions = self.submission_repo.count(form_id=form_id)
# Аналитика по каждому полю
fields_analytics = {}
for form_field in form.fields:
field = form_field.field
stats = self.submission_repo.get_field_statistics(form_id, field.name)
fields_analytics[field.name] = {
"label": field.label,
"type": field.field_type,
"completion_rate": self._calculate_completion_rate(form_id, field.name),
"unique_values": len(stats),
"distribution": stats,
"null_count": self._get_null_count(form_id, field.name)
}
# Временная аналитика
daily_stats = self.submission_repo.get_daily_submissions(form_id)
return {
"form_name": form.name,
"total_submissions": total_submissions,
"fields_analytics": fields_analytics,
"daily_statistics": daily_stats,
"peak_hours": self._get_peak_hours(form_id),
"submission_trend": self._calculate_trend(daily_stats)
}
def _calculate_completion_rate(self, form_id: int, field_name: str) -> float:
"""Процент заполнения поля"""
total = self.submission_repo.count(form_id=form_id)
if total == 0:
return 0.0
filled = self.submission_repo.count(
form_id=form_id,
filters={f"data->>'{field_name}'": "is not null"}
)
return (filled / total) * 100
def _get_null_count(self, form_id: int, field_name: str) -> int:
"""Количество пустых значений в поле"""
return self.submission_repo.count(
form_id=form_id,
filters={f"data->>'{field_name}'": "is null"}
)# app/services/analytics_service.py (базовая версия)
from sqlalchemy.orm import Session
from sqlalchemy import func
from datetime import datetime, timedelta
from app.models.submission import Submission
from app.models.form import Form
class AnalyticsService:
def __init__(self, db: Session):
self.db = db
def get_form_report(self, form_id: int, days: int = 30):
form = self.db.query(Form).filter(Form.id == form_id).first()
if not form:
raise ValueError("Form not found")
start_date = datetime.utcnow() - timedelta(days=days)
submissions = self.db.query(Submission).filter(
Submission.form_id == form_id,
Submission.submitted_at >= start_date
).all()
return {
"form_id": form_id,
"form_name": form.name,
"total_submissions": len(submissions),
"unique_submitters": len(set(s.submitted_by for s in submissions if s.submitted_by)),
"avg_completion_time": None,
"completion_rate": 100.0,
"fields_stats": [],
"daily_stats": [],
"peak_hours": {},
"trend": "stable",
"last_submission_at": submissions[-1].submitted_at if submissions else None,
"first_submission_at": submissions[0].submitted_at if submissions else None
}
def get_field_statistics(self, form_id: int, field_name: str):
return None
def get_global_overview(self, days: int):
return {"total_forms": 0, "total_submissions": 0, "active_forms": 0}
def _get_peak_hours(self, form_id: int) -> Dict[str, int]:
"""Часы пик заполнения"""
from sqlalchemy import func
results = self.db.query(
func.extract('hour', Submission.submitted_at).label('hour'),
func.count(Submission.id).label('count')
).filter(
Submission.form_id == form_id
).group_by('hour').all()
return {int(r.hour): r.count for r in results}
def _calculate_trend(self, daily_stats: List[Dict]) -> str:
"""Расчет тренда (рост/падение/стабильно)"""
if len(daily_stats) < 2:
return "insufficient_data"
recent_avg = sum(d['count'] for d in daily_stats[-7:]) / 7
previous_avg = sum(d['count'] for d in daily_stats[-14:-7]) / 7
if recent_avg > previous_avg * 1.1:
return "increasing"
elif recent_avg < previous_avg * 0.9:
return "decreasing"
else:
return "stable"

View File

@@ -0,0 +1,19 @@
# app/services/export_service.py (базовая версия)
from sqlalchemy.orm import Session
import csv
import io
class ExportService:
def __init__(self, db: Session):
self.db = db
def export_form_data(self, form_id: int, format: str = "csv"):
# Временная заглушка
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(["id", "form_id", "submitted_at", "data"])
writer.writerow(["1", str(form_id), "2024-01-01", "{}"])
output.seek(0)
return output.getvalue(), f"form_{form_id}.csv", "text/csv"

View File

@@ -0,0 +1,145 @@
# app/services/form_service.py
from sqlalchemy.orm import Session
from typing import Optional, List, Dict, Any
from app.models.form import Form, Field, FormField
from app.schemas.form import FormCreate
class FormService:
def __init__(self, db: Session):
self.db = db
def create_form(self, form_data: FormCreate, user_id=None):
"""Создать новую форму с полями"""
# 1. Создаем форму
form = Form(
name=form_data.name,
description=form_data.description,
version=1,
is_active=True,
is_published=False,
created_by=user_id
)
self.db.add(form)
self.db.flush()
# 2. Создаем или получаем поля и связываем с формой
for field_data in form_data.fields:
existing_field = self.db.query(Field).filter(
Field.name == field_data["name"]
).first()
if existing_field:
field = existing_field
else:
field = Field(
name=field_data["name"],
label=field_data["label"],
field_type=field_data["field_type"],
placeholder=field_data.get("placeholder"),
help_text=field_data.get("help_text"),
field_options=field_data.get("options", {}),
validation_rules=field_data.get("validation_rules", {}),
field_metadata=field_data.get("metadata", {})
)
self.db.add(field)
self.db.flush()
form_field = FormField(
form_id=form.id,
field_id=field.id,
order=field_data.get("order", 0),
is_required=field_data.get("is_required", False)
)
self.db.add(form_field)
self.db.commit()
self.db.refresh(form)
return form
def get_form_by_id(self, form_id: int, include_fields: bool = True):
"""Получить форму с полями"""
form = self.db.query(Form).filter(Form.id == form_id).first()
if not form:
return None
return form
def get_form_with_fields(self, form_id: int):
"""Получить форму с полями в виде словаря (для API)"""
form = self.db.query(Form).filter(Form.id == form_id).first()
if not form:
return None
# Получаем поля формы
form_fields = self.db.query(FormField).filter(
FormField.form_id == form_id
).order_by(FormField.order).all()
fields_list = []
for ff in form_fields:
field = self.db.query(Field).filter(Field.id == ff.field_id).first()
if field:
fields_list.append({
"id": field.id,
"name": field.name,
"label": field.label,
"field_type": field.field_type,
"order": ff.order,
"is_required": ff.is_required,
"placeholder": field.placeholder,
"help_text": field.help_text,
"options": field.field_options or {},
"validation_rules": field.validation_rules or {},
"metadata": field.field_metadata or {},
"created_at": field.created_at.isoformat() if field.created_at else None
})
# Возвращаем словарь, а не объект SQLAlchemy
return {
"id": form.id,
"name": form.name,
"description": form.description,
"version": form.version,
"is_active": form.is_active,
"is_published": form.is_published,
"created_at": form.created_at.isoformat() if form.created_at else None,
"updated_at": form.updated_at.isoformat() if form.updated_at else None,
"created_by": form.created_by,
"fields": fields_list
}
def get_forms_paginated(self, page=1, per_page=20, active_only=False):
"""Получить список форм с пагинацией"""
query = self.db.query(Form)
if active_only:
query = query.filter(Form.is_active == True)
total = query.count()
items = query.offset((page - 1) * per_page).limit(per_page).all()
# Преобразуем объекты в словари
items_list = []
for form in items:
items_list.append({
"id": form.id,
"name": form.name,
"description": form.description,
"version": form.version,
"is_active": form.is_active,
"is_published": form.is_published,
"created_at": form.created_at.isoformat() if form.created_at else None,
"updated_at": form.updated_at.isoformat() if form.updated_at else None,
"created_by": form.created_by
})
return {
"items": items_list,
"total": total,
"page": page,
"per_page": per_page,
"pages": (total + per_page - 1) // per_page
}

View File

@@ -0,0 +1,115 @@
# app/services/submission_service.py
from sqlalchemy.orm import Session
from typing import Optional, Dict, Any
from app.models.submission import Submission
from app.schemas.submission import SubmissionCreate
class SubmissionService:
def __init__(self, db: Session):
self.db = db
def create_submission(self, submission_data: SubmissionCreate):
"""Создать новую submission"""
submission = Submission(
form_id=submission_data.form_id,
submission_data=submission_data.data, # ← используем submission_data
submission_metadata=submission_data.metadata.dict() if submission_data.metadata else {},
status="completed"
)
self.db.add(submission)
self.db.commit()
self.db.refresh(submission)
# Возвращаем словарь, соответствующий схеме
return {
"id": submission.id,
"submission_uuid": str(submission.submission_uuid),
"form_id": submission.form_id,
"submission_data": submission.submission_data,
"submission_metadata": submission.submission_metadata,
"status": submission.status,
"submitted_at": submission.submitted_at,
"created_at": submission.created_at,
"updated_at": submission.updated_at,
"submitted_by": submission.submitted_by,
"completion_time_seconds": submission.completion_time_seconds,
"referrer": submission.referrer
}
def get_submission_by_id(self, submission_id: int):
"""Получить submission по ID"""
submission = self.db.query(Submission).filter(Submission.id == submission_id).first()
if not submission:
return None
return {
"id": submission.id,
"submission_uuid": str(submission.submission_uuid),
"form_id": submission.form_id,
"submission_data": submission.submission_data,
"submission_metadata": submission.submission_metadata,
"status": submission.status,
"submitted_at": submission.submitted_at,
"created_at": submission.created_at,
"updated_at": submission.updated_at,
"submitted_by": submission.submitted_by,
"completion_time_seconds": submission.completion_time_seconds,
"referrer": submission.referrer
}
def get_submissions_by_form(self, form_id: int, page=1, per_page=20, status=None):
"""Получить все submission формы"""
query = self.db.query(Submission).filter(Submission.form_id == form_id)
if status:
query = query.filter(Submission.status == status)
total = query.count()
items = query.offset((page - 1) * per_page).limit(per_page).all()
items_list = []
for submission in items:
items_list.append({
"id": submission.id,
"submission_uuid": str(submission.submission_uuid),
"form_id": submission.form_id,
"submission_data": submission.submission_data,
"submission_metadata": submission.submission_metadata,
"status": submission.status,
"submitted_at": submission.submitted_at,
"created_at": submission.created_at,
"updated_at": submission.updated_at,
"submitted_by": submission.submitted_by,
"completion_time_seconds": submission.completion_time_seconds,
"referrer": submission.referrer
})
return {
"items": items_list,
"total": total,
"page": page,
"per_page": per_page,
"pages": (total + per_page - 1) // per_page
}
def update_submission(self, submission_id: int, data: Dict[str, Any]):
"""Обновить submission"""
submission = self.db.query(Submission).filter(Submission.id == submission_id).first()
if not submission:
return None
submission.submission_data = data
self.db.commit()
self.db.refresh(submission)
return self.get_submission_by_id(submission_id)
def delete_submission(self, submission_id: int):
"""Удалить submission"""
submission = self.db.query(Submission).filter(Submission.id == submission_id).first()
if not submission:
return False
self.db.delete(submission)
self.db.commit()
return True