This commit is contained in:
2026-03-01 21:40:09 +05:00
parent d37c255994
commit a1dd1a3279
10 changed files with 186 additions and 119 deletions

View File

@@ -7,6 +7,7 @@ from typing import List
from bd import Settings
from sqlalchemy import create_engine
from sqlalchemy.exc import SQLAlchemyError
from pydantic import BaseModel
router = APIRouter(tags=["db"])
@@ -67,3 +68,40 @@ async def create_tables():
return {"status": "ok", "detail": f"Created {len(unique)} metadata groups"}
except SQLAlchemyError as e:
raise HTTPException(status_code=500, detail=str(e))
class ClearDBIn(BaseModel):
confirm: bool
@router.post("/db/clear")
async def clear_tables(payload: ClearDBIn):
"""Полная очистка всех таблиц, описанных в `bd.tables`.
Требуется явное подтверждение: POST с телом {"confirm": true}.
"""
if not payload.confirm:
raise HTTPException(status_code=400, detail="Confirmation required")
settings = Settings()
db_url = settings.DATABASE_URL_syncpg
engine = create_engine(db_url, future=True)
metadatas = collect_metadatas()
if not metadatas:
raise HTTPException(status_code=400, detail="No table metadata found in db.tables")
# deduplicate metadata objects
unique = []
seen = set()
for md in metadatas:
if id(md) not in seen:
seen.add(id(md))
unique.append(md)
try:
for md in unique:
md.drop_all(bind=engine)
return {"status": "ok", "detail": f"Dropped {len(unique)} metadata groups"}
except SQLAlchemyError as e:
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -95,3 +95,15 @@ def get_response(response_id: str):
if not r:
raise HTTPException(status_code=404, detail="Response not found")
return {"id": str(r.id), "poll_id": str(r.poll_id), "user_id": str(r.user_id) if r.user_id else None, "submitted_at": r.submitted_at.isoformat(), "answers": [{"id": str(a.id), "question_id": str(a.question_id), "choice_id": str(a.choice_id) if a.choice_id else None, "text": a.text} for a in r.answers]}
@router.get("/users/{user_id}/responses")
def list_responses_by_user(user_id: str):
Session = get_session()
try:
uid = uuid.UUID(user_id)
except Exception:
raise HTTPException(status_code=400, detail="Invalid user UUID")
with Session() as session:
rows = session.query(Response).filter(Response.user_id == uid).all()
return [{"id": str(r.id), "poll_id": str(r.poll_id), "submitted_at": r.submitted_at.isoformat()} for r in rows]

View File

@@ -1,111 +0,0 @@
from fastapi import APIRouter, Request
router = APIRouter()
@router.get("/ui/create-user")
def create_user_page(request: Request):
return """
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Create user & Take test</title>
</head>
<body>
<h1>Create user</h1>
<form id="userForm">
<label>First name: <input name="first_name" required></label><br>
<label>Last name: <input name="last_name" required></label><br>
<button type="submit">Create user</button>
</form>
<h2>Or create Holland test</h2>
<button id="createHolland">Create Holland test</button>
<script>
document.getElementById('userForm').addEventListener('submit', async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
const data = { first_name: fd.get('first_name'), last_name: fd.get('last_name') };
const res = await fetch('/users/', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(data) });
if (!res.ok) { alert('Create user failed'); return; }
const js = await res.json();
const uid = js.id;
alert('User created: ' + uid);
// allow user to input poll id manually
const poll = prompt('Enter poll id to take (or leave empty to create Holland test)');
if (poll) {
location.href = '/ui/take-test?poll_id=' + encodeURIComponent(poll) + '&user_id=' + encodeURIComponent(uid);
} else {
// create Holland test then redirect
const r2 = await fetch('/polls/holland', { method: 'POST' });
const p = await r2.json();
location.href = '/ui/take-test?poll_id=' + encodeURIComponent(p.id) + '&user_id=' + encodeURIComponent(uid);
}
});
document.getElementById('createHolland').addEventListener('click', async () => {
const r = await fetch('/polls/holland', { method: 'POST' });
if (!r.ok) { alert('Create failed'); return; }
const p = await r.json();
alert('Created poll ' + p.id);
location.href = '/ui/take-test?poll_id=' + encodeURIComponent(p.id);
});
</script>
</body>
</html>
"""
@router.get("/ui/take-test")
def take_test_page(request: Request, poll_id: str = None, user_id: str = None):
return f"""
<!doctype html>
<html>
<head><meta charset="utf-8"><title>Take test</title></head>
<body>
<h1>Take test</h1>
<div id="poll"></div>
<button id="submitBtn">Submit</button>
<script>
const pollId = '{poll_id or ''}';
const userId = '{user_id or ''}';
async function load() {{
if (!pollId) {{ document.getElementById('poll').innerText = 'No poll_id provided'; return; }}
const r = await fetch('/polls/' + encodeURIComponent(pollId));
if (!r.ok) {{ document.getElementById('poll').innerText = 'Failed to load poll'; return; }}
const p = await r.json();
const container = document.getElementById('poll');
const h = document.createElement('h2'); h.innerText = p.title; container.appendChild(h);
p.questions.forEach(q => {{
const div = document.createElement('div');
div.innerHTML = '<p>' + q.text + '</p>';
q.choices.forEach(c => {{
const id = 'q_' + q.id + '_c_' + c.id;
const input = `<label><input type="radio" name="${{q.id}}" value="${{c.id}}"> ${{c.text}}</label><br>`;
div.insertAdjacentHTML('beforeend', input);
}});
container.appendChild(div);
}});
}}
load();
document.getElementById('submitBtn').addEventListener('click', async () => {{
const answers = [];
const groups = new Set();
document.querySelectorAll('input[type=radio]').forEach(r => groups.add(r.name));
groups.forEach(name => {{
const checked = document.querySelector('input[name="' + name + '"]:checked');
if (checked) answers.push({{question_id: name, choice_id: checked.value}});
}});
const payload = {{ user_id: userId || null, answers }};
const res = await fetch('/polls/' + encodeURIComponent(pollId) + '/responses', {{ method: 'POST', headers: {{'Content-Type':'application/json'}}, body: JSON.stringify(payload) }});
if (!res.ok) {{ alert('Submit failed'); return; }}
const js = await res.json();
alert('Response saved: ' + js.id);
}});
</script>
</body>
</html>
"""

View File

@@ -13,8 +13,9 @@ router = APIRouter(tags=["users"], prefix="/users")
class UserCreate(BaseModel):
first_name: str
last_name: str
first_name: Optional[str] = None
last_name: Optional[str] = None
name: Optional[str] = None
class UserUpdate(BaseModel):
@@ -28,11 +29,21 @@ def get_session():
return sessionmaker(bind=engine, future=True)
@router.post("/", status_code=201)
@router.post("", status_code=201)
def create_user(payload: UserCreate):
Session = get_session()
with Session() as session:
user = User(first_name=payload.first_name, last_name=payload.last_name)
# Support payload with 'name' (single field) or first_name+last_name
first = payload.first_name
last = payload.last_name
if not (first or last) and payload.name:
parts = payload.name.strip().split()
if parts:
first = parts[0]
last = " ".join(parts[1:]) if len(parts) > 1 else ""
if not first and not last:
raise HTTPException(status_code=422, detail="Missing name or first_name/last_name")
user = User(first_name=first or "", last_name=last or "")
session.add(user)
session.commit()
session.refresh(user)
@@ -76,7 +87,7 @@ def delete_user(user_id: str):
return {}
@router.get("/")
@router.get("")
def list_users():
Session = get_session()
with Session() as session: