Harden auth and persist report schedules
This commit is contained in:
@@ -715,7 +715,7 @@ async def generate_questions_batch(
|
||||
return generated_items
|
||||
|
||||
|
||||
async def get_ai_stats(db: AsyncSession) -> Dict[str, Any]:
|
||||
async def get_ai_stats(db: AsyncSession, website_id: int | None = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Get AI generation statistics.
|
||||
|
||||
@@ -725,16 +725,18 @@ async def get_ai_stats(db: AsyncSession) -> Dict[str, Any]:
|
||||
Returns:
|
||||
Statistics dictionary
|
||||
"""
|
||||
filters = [Item.generated_by == "ai"]
|
||||
if website_id is not None:
|
||||
filters.append(Item.website_id == website_id)
|
||||
|
||||
# Total AI-generated items
|
||||
total_result = await db.execute(
|
||||
select(func.count(Item.id)).where(Item.generated_by == "ai")
|
||||
)
|
||||
total_result = await db.execute(select(func.count(Item.id)).where(*filters))
|
||||
total_ai_items = total_result.scalar() or 0
|
||||
|
||||
# Items by model
|
||||
model_result = await db.execute(
|
||||
select(Item.ai_model, func.count(Item.id))
|
||||
.where(Item.generated_by == "ai")
|
||||
.where(*filters)
|
||||
.where(Item.ai_model.isnot(None))
|
||||
.group_by(Item.ai_model)
|
||||
)
|
||||
|
||||
@@ -23,6 +23,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.models.item import Item
|
||||
from app.models.report_schedule import ReportScheduleModel
|
||||
from app.models.session import Session
|
||||
from app.models.tryout import Tryout
|
||||
from app.models.tryout_stats import TryoutStats
|
||||
@@ -256,7 +257,8 @@ async def generate_student_performance_report(
|
||||
website_id: int,
|
||||
db: AsyncSession,
|
||||
date_range: Optional[Dict[str, datetime]] = None,
|
||||
format_type: Literal["individual", "aggregate", "both"] = "both"
|
||||
format_type: Literal["individual", "aggregate", "both"] = "both",
|
||||
wp_user_id: Optional[str] = None,
|
||||
) -> StudentPerformanceReport:
|
||||
"""
|
||||
Generate student performance report.
|
||||
@@ -267,6 +269,7 @@ async def generate_student_performance_report(
|
||||
db: Database session
|
||||
date_range: Optional date range filter {"start": datetime, "end": datetime}
|
||||
format_type: Report format - individual, aggregate, or both
|
||||
wp_user_id: Optional WordPress user filter for student-scoped reports
|
||||
|
||||
Returns:
|
||||
StudentPerformanceReport with aggregate stats and/or individual records
|
||||
@@ -287,6 +290,9 @@ async def generate_student_performance_report(
|
||||
query = query.where(Session.start_time >= date_range["start"])
|
||||
if date_range.get("end"):
|
||||
query = query.where(Session.start_time <= date_range["end"])
|
||||
|
||||
if wp_user_id is not None:
|
||||
query = query.where(Session.wp_user_id == wp_user_id)
|
||||
|
||||
query = query.order_by(Session.NN.desc().nullslast())
|
||||
|
||||
@@ -1382,11 +1388,34 @@ class ReportSchedule:
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
# In-memory store for scheduled reports (in production, use database)
|
||||
_scheduled_reports: Dict[str, ReportSchedule] = {}
|
||||
def _calculate_next_run(schedule: Literal["daily", "weekly", "monthly"]) -> datetime:
|
||||
now = datetime.now(timezone.utc)
|
||||
if schedule == "daily":
|
||||
return now + timedelta(days=1)
|
||||
if schedule == "weekly":
|
||||
return now + timedelta(weeks=1)
|
||||
return now + timedelta(days=30)
|
||||
|
||||
|
||||
def schedule_report(
|
||||
def _schedule_from_model(row: ReportScheduleModel) -> ReportSchedule:
|
||||
return ReportSchedule(
|
||||
schedule_id=row.schedule_id,
|
||||
report_type=row.report_type,
|
||||
schedule=row.schedule,
|
||||
tryout_ids=list(row.tryout_ids or []),
|
||||
website_id=row.website_id,
|
||||
recipients=list(row.recipients or []),
|
||||
format=row.format,
|
||||
created_at=row.created_at,
|
||||
last_run=row.last_run,
|
||||
next_run=row.next_run,
|
||||
is_active=row.is_active,
|
||||
)
|
||||
|
||||
|
||||
async def schedule_report(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
report_type: Literal["student_performance", "item_analysis", "calibration_status", "tryout_comparison"],
|
||||
schedule: Literal["daily", "weekly", "monthly"],
|
||||
tryout_ids: List[str],
|
||||
@@ -1412,16 +1441,7 @@ def schedule_report(
|
||||
|
||||
schedule_id = str(uuid.uuid4())
|
||||
|
||||
# Calculate next run time
|
||||
now = datetime.now(timezone.utc)
|
||||
if schedule == "daily":
|
||||
next_run = now + timedelta(days=1)
|
||||
elif schedule == "weekly":
|
||||
next_run = now + timedelta(weeks=1)
|
||||
else: # monthly
|
||||
next_run = now + timedelta(days=30)
|
||||
|
||||
report_schedule = ReportSchedule(
|
||||
report_schedule = ReportScheduleModel(
|
||||
schedule_id=schedule_id,
|
||||
report_type=report_type,
|
||||
schedule=schedule,
|
||||
@@ -1429,35 +1449,54 @@ def schedule_report(
|
||||
website_id=website_id,
|
||||
recipients=recipients,
|
||||
format=export_format,
|
||||
next_run=next_run,
|
||||
next_run=_calculate_next_run(schedule),
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
_scheduled_reports[schedule_id] = report_schedule
|
||||
db.add(report_schedule)
|
||||
await db.flush()
|
||||
logger.info(f"Scheduled report {schedule_id}: {report_type} {schedule}")
|
||||
|
||||
return schedule_id
|
||||
|
||||
|
||||
def get_scheduled_report(schedule_id: str) -> Optional[ReportSchedule]:
|
||||
async def get_scheduled_report(db: AsyncSession, schedule_id: str) -> Optional[ReportSchedule]:
|
||||
"""Get a scheduled report by ID."""
|
||||
return _scheduled_reports.get(schedule_id)
|
||||
result = await db.execute(
|
||||
select(ReportScheduleModel).where(ReportScheduleModel.schedule_id == schedule_id)
|
||||
)
|
||||
row = result.scalar_one_or_none()
|
||||
return _schedule_from_model(row) if row else None
|
||||
|
||||
|
||||
def list_scheduled_reports(website_id: Optional[int] = None) -> List[ReportSchedule]:
|
||||
async def list_scheduled_reports(
|
||||
db: AsyncSession,
|
||||
website_id: Optional[int] = None,
|
||||
) -> List[ReportSchedule]:
|
||||
"""List all scheduled reports, optionally filtered by website."""
|
||||
reports = list(_scheduled_reports.values())
|
||||
if website_id:
|
||||
reports = [r for r in reports if r.website_id == website_id]
|
||||
return reports
|
||||
query = (
|
||||
select(ReportScheduleModel)
|
||||
.where(ReportScheduleModel.is_active == True)
|
||||
.order_by(ReportScheduleModel.created_at.desc())
|
||||
)
|
||||
if website_id is not None:
|
||||
query = query.where(ReportScheduleModel.website_id == website_id)
|
||||
result = await db.execute(query)
|
||||
return [_schedule_from_model(row) for row in result.scalars().all()]
|
||||
|
||||
|
||||
def cancel_scheduled_report(schedule_id: str) -> bool:
|
||||
async def cancel_scheduled_report(db: AsyncSession, schedule_id: str) -> bool:
|
||||
"""Cancel a scheduled report."""
|
||||
if schedule_id in _scheduled_reports:
|
||||
del _scheduled_reports[schedule_id]
|
||||
logger.info(f"Cancelled scheduled report {schedule_id}")
|
||||
return True
|
||||
return False
|
||||
result = await db.execute(
|
||||
select(ReportScheduleModel).where(ReportScheduleModel.schedule_id == schedule_id)
|
||||
)
|
||||
row = result.scalar_one_or_none()
|
||||
if row is None:
|
||||
return False
|
||||
row.is_active = False
|
||||
await db.flush()
|
||||
logger.info(f"Cancelled scheduled report {schedule_id}")
|
||||
return True
|
||||
|
||||
|
||||
# Export public API
|
||||
|
||||
Reference in New Issue
Block a user