first
This commit is contained in:
12
app/services/__init__.py
Normal file
12
app/services/__init__.py
Normal 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"
|
||||
]
|
||||
135
app/services/analytics_service.py
Normal file
135
app/services/analytics_service.py
Normal 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"
|
||||
19
app/services/export_service.py
Normal file
19
app/services/export_service.py
Normal 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"
|
||||
145
app/services/form_service.py
Normal file
145
app/services/form_service.py
Normal 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
|
||||
}
|
||||
115
app/services/submission_service.py
Normal file
115
app/services/submission_service.py
Normal 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
|
||||
Reference in New Issue
Block a user