fix: harden admin access, repair ORM joins, and add migration/tests

This commit is contained in:
dwindown
2026-04-01 14:59:54 +07:00
parent de592d140e
commit 16ab13e911
21 changed files with 1275 additions and 368 deletions

View File

@@ -14,7 +14,7 @@ import math
from datetime import datetime, timezone
from typing import Optional
from sqlalchemy import func, select
from sqlalchemy import Integer, cast, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.item import Item
@@ -190,7 +190,7 @@ async def calculate_ctt_p_for_item(
result = await db.execute(
select(
func.count().label("total"),
func.sum(func.cast(UserAnswer.is_correct, type_=func.INTEGER)).label("correct"),
func.sum(cast(UserAnswer.is_correct, Integer)).label("correct"),
).where(UserAnswer.item_id == item_id)
)
row = result.first()

View File

@@ -308,7 +308,7 @@ async def get_normalization_params(
Tryout.tryout_id == tryout_id,
)
)
row = result.scalar_one_or_none()
row = result.one_or_none()
if row is None:
raise ValueError(
@@ -352,7 +352,7 @@ async def get_normalization_params(
Tryout.tryout_id == tryout_id,
)
)
row = result.scalar_one_or_none()
row = result.one_or_none()
if row is None:
raise ValueError(
f"Tryout {tryout_id} not found for website {website_id}"
@@ -369,7 +369,7 @@ async def get_normalization_params(
Tryout.tryout_id == tryout_id,
)
)
row = result.scalar_one_or_none()
row = result.one_or_none()
if row is None:
raise ValueError(
f"Tryout {tryout_id} not found for website {website_id}"

View File

@@ -18,7 +18,7 @@ from dataclasses import dataclass, field
import logging
import pandas as pd
from sqlalchemy import select, func, and_, or_
from sqlalchemy import Integer, and_, cast, func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -415,7 +415,7 @@ async def generate_item_analysis_report(
resp_result = await db.execute(
select(
func.count().label("total"),
func.sum(func.cast(UserAnswer.is_correct, type_=func.INTEGER)).label("correct")
func.sum(cast(UserAnswer.is_correct, Integer)).label("correct")
).where(UserAnswer.item_id == item.id)
)
resp_stats = resp_result.first()
@@ -678,7 +678,7 @@ async def generate_tryout_comparison_report(
cal_result = await db.execute(
select(
func.count().label("total"),
func.sum(func.cast(Item.calibrated, type_=func.INTEGER)).label("calibrated")
func.sum(cast(Item.calibrated, Integer)).label("calibrated")
).where(
Item.tryout_id == tryout_id,
Item.website_id == website_id,
@@ -704,15 +704,56 @@ async def generate_tryout_comparison_report(
if tryout:
date_str = tryout.created_at.strftime("%Y-%m-%d")
session_result = await db.execute(
select(
func.count(Session.id).label("participant_count"),
func.avg(Session.NM).label("avg_nm"),
func.avg(Session.NN).label("avg_nn"),
func.avg(Session.theta).label("avg_theta"),
func.stddev_pop(Session.NM).label("std_nm"),
).where(
Session.tryout_id == tryout_id,
Session.website_id == website_id,
Session.is_completed.is_(True),
)
)
session_stats = session_result.first()
participant_count = (
int(session_stats.participant_count)
if session_stats and session_stats.participant_count
else (stats.participant_count if stats else 0)
)
avg_nm = (
round(float(session_stats.avg_nm), 2)
if session_stats and session_stats.avg_nm is not None
else (round(float(stats.rataan), 2) if stats and stats.rataan is not None else None)
)
avg_nn = (
round(float(session_stats.avg_nn), 2)
if session_stats and session_stats.avg_nn is not None
else None
)
avg_theta = (
round(float(session_stats.avg_theta), 4)
if session_stats and session_stats.avg_theta is not None
else None
)
std_nm = (
round(float(session_stats.std_nm), 2)
if session_stats and session_stats.std_nm is not None
else (round(float(stats.sb), 2) if stats and stats.sb is not None else None)
)
record = TryoutComparisonRecord(
tryout_id=tryout_id,
date=date_str,
subject=subject,
participant_count=stats.participant_count if stats else 0,
avg_nm=round(stats.rataan, 2) if stats and stats.rataan else None,
avg_nn=round(stats.rataan + 500, 2) if stats and stats.rataan else None,
avg_theta=None, # Would need to calculate from sessions
std_nm=round(stats.sb, 2) if stats and stats.sb else None,
participant_count=participant_count,
avg_nm=avg_nm,
avg_nn=avg_nn,
avg_theta=avg_theta,
std_nm=std_nm,
calibration_percentage=round(cal_percentage, 2),
)
comparison_records.append(record)