fix data base
This commit is contained in:
1
route/__init__.py
Normal file
1
route/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Routes package
|
||||
@@ -1,6 +1,10 @@
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
from fastapi import APIRouter
|
||||
from fastapi import APIRouter, HTTPException
|
||||
import socket
|
||||
|
||||
from bd import Settings
|
||||
from redis_db import get_redis_client
|
||||
|
||||
# Создаем базовый router для FastAPI
|
||||
router = APIRouter(tags=["base"])
|
||||
@@ -25,4 +29,36 @@ async def health():
|
||||
|
||||
@router.get("/version")
|
||||
async def version():
|
||||
return {"version": get_version()}
|
||||
return {"version": get_version()}
|
||||
|
||||
|
||||
@router.get("/health/db")
|
||||
async def health_db():
|
||||
"""Проверка доступности БД по TCP (host:port)"""
|
||||
settings = Settings()
|
||||
host = settings.DB_HOST
|
||||
port = settings.DB_PORT
|
||||
try:
|
||||
# Пробуем открыть TCP-соединение к хосту:порту
|
||||
with socket.create_connection((host, port), timeout=3):
|
||||
return {"status": "ok", "db": "reachable", "host": host, "port": port}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=503, detail=f"db unreachable: {e}")
|
||||
|
||||
|
||||
@router.get("/health/redis")
|
||||
async def health_redis():
|
||||
"""Проверка подключения к Redis через ping()"""
|
||||
client = get_redis_client()
|
||||
try:
|
||||
if client.ping():
|
||||
# Получаем параметры соединения для удобства в ответе
|
||||
conn_kwargs = getattr(client, "connection_pool", None)
|
||||
info = {}
|
||||
if conn_kwargs:
|
||||
info = conn_kwargs.connection_kwargs if hasattr(conn_kwargs, 'connection_kwargs') else {}
|
||||
return {"status": "ok", "redis": "reachable", "info": info}
|
||||
else:
|
||||
raise HTTPException(status_code=503, detail="redis ping failed")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=503, detail=f"redis unreachable: {e}")
|
||||
114
route/choice_crud.py
Normal file
114
route/choice_crud.py
Normal file
@@ -0,0 +1,114 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
import uuid
|
||||
|
||||
from bd import Settings
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from bd.tables.choice import Choice
|
||||
from bd.tables.question import Question
|
||||
|
||||
router = APIRouter(tags=["choices"], prefix="/choices")
|
||||
|
||||
|
||||
class ChoiceCreate(BaseModel):
|
||||
question_id: str
|
||||
text: str
|
||||
position: Optional[int]
|
||||
|
||||
|
||||
class ChoiceUpdate(BaseModel):
|
||||
text: Optional[str]
|
||||
position: Optional[int]
|
||||
|
||||
|
||||
def get_session():
|
||||
settings = Settings()
|
||||
engine = create_engine(settings.DATABASE_URL_syncpg, future=True)
|
||||
return sessionmaker(bind=engine, future=True)
|
||||
|
||||
|
||||
@router.post("/", status_code=201)
|
||||
def create_choice(payload: ChoiceCreate):
|
||||
Session = get_session()
|
||||
try:
|
||||
qid = uuid.UUID(payload.question_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid question UUID")
|
||||
with Session() as session:
|
||||
question = session.get(Question, qid)
|
||||
if not question:
|
||||
raise HTTPException(status_code=404, detail="Question not found")
|
||||
ch = Choice(question_id=qid, text=payload.text, position=payload.position)
|
||||
session.add(ch)
|
||||
session.commit()
|
||||
session.refresh(ch)
|
||||
return {"id": str(ch.id)}
|
||||
|
||||
|
||||
@router.put("/{choice_id}")
|
||||
def update_choice(choice_id: str, payload: ChoiceUpdate):
|
||||
Session = get_session()
|
||||
try:
|
||||
cid = uuid.UUID(choice_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
ch = session.get(Choice, cid)
|
||||
if not ch:
|
||||
raise HTTPException(status_code=404, detail="Choice not found")
|
||||
if payload.text is not None:
|
||||
ch.text = payload.text
|
||||
if payload.position is not None:
|
||||
ch.position = payload.position
|
||||
session.add(ch)
|
||||
session.commit()
|
||||
session.refresh(ch)
|
||||
return {"id": str(ch.id)}
|
||||
|
||||
|
||||
@router.delete("/{choice_id}", status_code=204)
|
||||
def delete_choice(choice_id: str):
|
||||
Session = get_session()
|
||||
try:
|
||||
cid = uuid.UUID(choice_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
ch = session.get(Choice, cid)
|
||||
if not ch:
|
||||
raise HTTPException(status_code=404, detail="Choice not found")
|
||||
session.delete(ch)
|
||||
session.commit()
|
||||
return {}
|
||||
|
||||
|
||||
@router.get("/")
|
||||
def list_choices(question_id: Optional[str] = None):
|
||||
Session = get_session()
|
||||
with Session() as session:
|
||||
q = session.query(Choice)
|
||||
if question_id:
|
||||
try:
|
||||
qid = uuid.UUID(question_id)
|
||||
q = q.filter(Choice.question_id == qid)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid question_id UUID")
|
||||
rows = q.all()
|
||||
return [{"id": str(r.id), "text": r.text, "question_id": str(r.question_id)} for r in rows]
|
||||
|
||||
|
||||
@router.get("/{choice_id}")
|
||||
def get_choice(choice_id: str):
|
||||
Session = get_session()
|
||||
try:
|
||||
cid = uuid.UUID(choice_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
ch = session.get(Choice, cid)
|
||||
if not ch:
|
||||
raise HTTPException(status_code=404, detail="Choice not found")
|
||||
return {"id": str(ch.id), "text": ch.text, "question_id": str(ch.question_id)}
|
||||
113
route/groups_crud.py
Normal file
113
route/groups_crud.py
Normal file
@@ -0,0 +1,113 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
import uuid
|
||||
|
||||
from bd import Settings
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from bd.tables.group import Group
|
||||
from bd.tables.users import User
|
||||
|
||||
router = APIRouter(tags=["groups"], prefix="/groups")
|
||||
|
||||
|
||||
class GroupCreate(BaseModel):
|
||||
name_group: str
|
||||
user_id: str
|
||||
|
||||
|
||||
class GroupUpdate(BaseModel):
|
||||
name_group: Optional[str]
|
||||
user_id: Optional[str]
|
||||
|
||||
|
||||
def get_session():
|
||||
settings = Settings()
|
||||
engine = create_engine(settings.DATABASE_URL_syncpg, future=True)
|
||||
return sessionmaker(bind=engine, future=True)
|
||||
|
||||
|
||||
@router.post("/", status_code=201)
|
||||
def create_group(payload: GroupCreate):
|
||||
Session = get_session()
|
||||
try:
|
||||
uid = uuid.UUID(payload.user_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid user_id UUID")
|
||||
with Session() as session:
|
||||
user = session.get(User, uid)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
grp = Group(name_group=payload.name_group, user_id=uid)
|
||||
session.add(grp)
|
||||
session.commit()
|
||||
session.refresh(grp)
|
||||
return {"id": str(grp.id), "name_group": grp.name_group, "user_id": str(grp.user_id)}
|
||||
|
||||
|
||||
@router.get("/")
|
||||
def list_groups():
|
||||
Session = get_session()
|
||||
with Session() as session:
|
||||
rows = session.query(Group).all()
|
||||
return [{"id": str(g.id), "name_group": g.name_group, "user_id": str(g.user_id)} for g in rows]
|
||||
|
||||
|
||||
@router.get("/{group_id}")
|
||||
def get_group(group_id: str):
|
||||
Session = get_session()
|
||||
try:
|
||||
gid = uuid.UUID(group_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
grp = session.get(Group, gid)
|
||||
if not grp:
|
||||
raise HTTPException(status_code=404, detail="Group not found")
|
||||
return {"id": str(grp.id), "name_group": grp.name_group, "user_id": str(grp.user_id)}
|
||||
|
||||
|
||||
@router.put("/{group_id}")
|
||||
def update_group(group_id: str, payload: GroupUpdate):
|
||||
Session = get_session()
|
||||
try:
|
||||
gid = uuid.UUID(group_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
grp = session.get(Group, gid)
|
||||
if not grp:
|
||||
raise HTTPException(status_code=404, detail="Group not found")
|
||||
if payload.name_group is not None:
|
||||
grp.name_group = payload.name_group
|
||||
if payload.user_id is not None:
|
||||
try:
|
||||
new_uid = uuid.UUID(payload.user_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid user_id UUID")
|
||||
user = session.get(User, new_uid)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
grp.user_id = new_uid
|
||||
session.add(grp)
|
||||
session.commit()
|
||||
session.refresh(grp)
|
||||
return {"id": str(grp.id), "name_group": grp.name_group, "user_id": str(grp.user_id)}
|
||||
|
||||
|
||||
@router.delete("/{group_id}", status_code=204)
|
||||
def delete_group(group_id: str):
|
||||
Session = get_session()
|
||||
try:
|
||||
gid = uuid.UUID(group_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
grp = session.get(Group, gid)
|
||||
if not grp:
|
||||
raise HTTPException(status_code=404, detail="Group not found")
|
||||
session.delete(grp)
|
||||
session.commit()
|
||||
return {}
|
||||
85
route/holland_crud.py
Normal file
85
route/holland_crud.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from typing import Optional
|
||||
import uuid
|
||||
|
||||
from bd import Settings
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from bd.tables.poll import Poll
|
||||
from bd.tables.question import Question
|
||||
from bd.tables.choice import Choice
|
||||
|
||||
router = APIRouter(tags=["polls"], prefix="/polls")
|
||||
|
||||
|
||||
PAIRS = [
|
||||
("инженер-техник", "инженер-контролер"),
|
||||
("вязальщик", "санитарный врач"),
|
||||
("повар", "наборщик"),
|
||||
("фотограф", "зав. магазином"),
|
||||
("чертежник", "дизайнер"),
|
||||
("философ", "психиатр"),
|
||||
("ученый-химик", "бухгалтер"),
|
||||
("редактор научного журнала", "адвокат"),
|
||||
("лингвист", "переводчик художественной литературы"),
|
||||
("педиатр", "статистик"),
|
||||
("организатор воспитательной работы", "председатель профсоюза"),
|
||||
("спортивный врач", "фельетонист"),
|
||||
("нотариус", "снабженец"),
|
||||
("перфоратор", "карикатурист"),
|
||||
("политический деятель", "писатель"),
|
||||
("садовник", "метеоролог"),
|
||||
("водитель", "медсестра"),
|
||||
("инженер-электрик", "секретарь-машинистка"),
|
||||
("маляр", "художник по металлу"),
|
||||
("биолог", "главный врач"),
|
||||
("телеоператор", "режиссер"),
|
||||
("гидролог", "ревизор"),
|
||||
("зоолог", "зоотехник"),
|
||||
("математик", "архитектор"),
|
||||
("работник ИДН", "счетовод"),
|
||||
("учитель", "милиционер"),
|
||||
("воспитатель", "художник по керамике"),
|
||||
("экономист", "заведующий отделом"),
|
||||
("корректор", "критик"),
|
||||
("завхоз", "директор"),
|
||||
("радиоинженер", "специалист по ядерной физике"),
|
||||
("водопроводчик", "наборщик"),
|
||||
("агроном", "председатель сельхозкооператива"),
|
||||
("закройщик-модельер", "декоратор"),
|
||||
("археолог", "эксперт"),
|
||||
("работник музея", "консультант"),
|
||||
("ученый", "актер"),
|
||||
("логопед", "стенографист"),
|
||||
("врач", "дипломат"),
|
||||
("главный бухгалтер", "директор"),
|
||||
("поэт", "психолог"),
|
||||
("архивариус", "скульптор"),
|
||||
]
|
||||
|
||||
|
||||
def get_session():
|
||||
settings = Settings()
|
||||
engine = create_engine(settings.DATABASE_URL_syncpg, future=True)
|
||||
return sessionmaker(bind=engine, future=True)
|
||||
|
||||
|
||||
@router.post("/holland", status_code=201)
|
||||
def create_holland_poll(author_id: Optional[str] = None):
|
||||
Session = get_session()
|
||||
with Session() as session:
|
||||
poll = Poll(title="Тест Д. Голланда — определение типа личности",
|
||||
description="Тест Д. Голланда: выберите из каждой пары предпочитаемую профессию.")
|
||||
# polls are anonymous; ignore author_id
|
||||
session.add(poll)
|
||||
for idx, (a, b) in enumerate(PAIRS, start=1):
|
||||
q = Question(text=f"Вариант {idx}", position=idx, type="single", poll=poll)
|
||||
session.add(q)
|
||||
ch1 = Choice(question=q, text=f"а) {a}", position=1)
|
||||
ch2 = Choice(question=q, text=f"б) {b}", position=2)
|
||||
session.add(ch1)
|
||||
session.add(ch2)
|
||||
session.commit()
|
||||
session.refresh(poll)
|
||||
return {"id": str(poll.id), "questions": len(poll.questions)}
|
||||
69
route/init_data_base.py
Normal file
69
route/init_data_base.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
import pkgutil
|
||||
import importlib
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from bd import Settings
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
router = APIRouter(tags=["db"])
|
||||
|
||||
|
||||
def _iter_table_modules() -> List[str]:
|
||||
try:
|
||||
import bd.tables as tables_pkg
|
||||
pkg_paths = getattr(tables_pkg, "__path__", None)
|
||||
if not pkg_paths:
|
||||
pkg_paths = [str(Path(__file__).resolve().parent.parent / "bd" / "tables")]
|
||||
except Exception:
|
||||
pkg_paths = [str(Path(__file__).resolve().parent.parent / "bd" / "tables")]
|
||||
|
||||
names = []
|
||||
for finder, name, ispkg in pkgutil.iter_modules(pkg_paths):
|
||||
names.append(name)
|
||||
return names
|
||||
|
||||
|
||||
def collect_metadatas():
|
||||
metadatas = []
|
||||
for mod_name in _iter_table_modules():
|
||||
try:
|
||||
module = importlib.import_module(f"bd.tables.{mod_name}")
|
||||
except Exception:
|
||||
continue
|
||||
Base = getattr(module, "Base", None)
|
||||
if Base is not None and hasattr(Base, "metadata"):
|
||||
metadatas.append(Base.metadata)
|
||||
return metadatas
|
||||
|
||||
|
||||
@router.post("/db/create-tables")
|
||||
async def create_tables():
|
||||
"""Создаёт все таблицы, описанные в модулях `db.tables`.
|
||||
|
||||
Endpoint вызывается по нажатию кнопки в UI (POST).
|
||||
"""
|
||||
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 (multiple modules may expose the same Base.metadata)
|
||||
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.create_all(bind=engine)
|
||||
return {"status": "ok", "detail": f"Created {len(unique)} metadata groups"}
|
||||
except SQLAlchemyError as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
113
route/organizations_crud.py
Normal file
113
route/organizations_crud.py
Normal file
@@ -0,0 +1,113 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
import uuid
|
||||
|
||||
from bd import Settings
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from bd.tables.organization import Organization
|
||||
from bd.tables.group import Group
|
||||
|
||||
router = APIRouter(tags=["organizations"], prefix="/organizations")
|
||||
|
||||
|
||||
class OrganizationCreate(BaseModel):
|
||||
name_organization: str
|
||||
group_id: str
|
||||
|
||||
|
||||
class OrganizationUpdate(BaseModel):
|
||||
name_organization: Optional[str]
|
||||
group_id: Optional[str]
|
||||
|
||||
|
||||
def get_session():
|
||||
settings = Settings()
|
||||
engine = create_engine(settings.DATABASE_URL_syncpg, future=True)
|
||||
return sessionmaker(bind=engine, future=True)
|
||||
|
||||
|
||||
@router.post("/", status_code=201)
|
||||
def create_organization(payload: OrganizationCreate):
|
||||
Session = get_session()
|
||||
try:
|
||||
gid = uuid.UUID(payload.group_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid group_id UUID")
|
||||
with Session() as session:
|
||||
grp = session.get(Group, gid)
|
||||
if not grp:
|
||||
raise HTTPException(status_code=404, detail="Group not found")
|
||||
org = Organization(name_organization=payload.name_organization, group_id=gid)
|
||||
session.add(org)
|
||||
session.commit()
|
||||
session.refresh(org)
|
||||
return {"id": str(org.id), "name_organization": org.name_organization, "group_id": str(org.group_id)}
|
||||
|
||||
|
||||
@router.get("/")
|
||||
def list_organizations():
|
||||
Session = get_session()
|
||||
with Session() as session:
|
||||
rows = session.query(Organization).all()
|
||||
return [{"id": str(o.id), "name_organization": o.name_organization, "group_id": str(o.group_id)} for o in rows]
|
||||
|
||||
|
||||
@router.get("/{org_id}")
|
||||
def get_organization(org_id: str):
|
||||
Session = get_session()
|
||||
try:
|
||||
oid = uuid.UUID(org_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
org = session.get(Organization, oid)
|
||||
if not org:
|
||||
raise HTTPException(status_code=404, detail="Organization not found")
|
||||
return {"id": str(org.id), "name_organization": org.name_organization, "group_id": str(org.group_id)}
|
||||
|
||||
|
||||
@router.put("/{org_id}")
|
||||
def update_organization(org_id: str, payload: OrganizationUpdate):
|
||||
Session = get_session()
|
||||
try:
|
||||
oid = uuid.UUID(org_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
org = session.get(Organization, oid)
|
||||
if not org:
|
||||
raise HTTPException(status_code=404, detail="Organization not found")
|
||||
if payload.name_organization is not None:
|
||||
org.name_organization = payload.name_organization
|
||||
if payload.group_id is not None:
|
||||
try:
|
||||
new_gid = uuid.UUID(payload.group_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid group_id UUID")
|
||||
grp = session.get(Group, new_gid)
|
||||
if not grp:
|
||||
raise HTTPException(status_code=404, detail="Group not found")
|
||||
org.group_id = new_gid
|
||||
session.add(org)
|
||||
session.commit()
|
||||
session.refresh(org)
|
||||
return {"id": str(org.id), "name_organization": org.name_organization, "group_id": str(org.group_id)}
|
||||
|
||||
|
||||
@router.delete("/{org_id}", status_code=204)
|
||||
def delete_organization(org_id: str):
|
||||
Session = get_session()
|
||||
try:
|
||||
oid = uuid.UUID(org_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
org = session.get(Organization, oid)
|
||||
if not org:
|
||||
raise HTTPException(status_code=404, detail="Organization not found")
|
||||
session.delete(org)
|
||||
session.commit()
|
||||
return {}
|
||||
95
route/poll_crud.py
Normal file
95
route/poll_crud.py
Normal file
@@ -0,0 +1,95 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
import uuid
|
||||
|
||||
from bd import Settings
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from bd.tables.poll import Poll
|
||||
from bd.tables.question import Question
|
||||
from bd.tables.choice import Choice
|
||||
|
||||
router = APIRouter(tags=["polls"], prefix="/polls")
|
||||
|
||||
|
||||
class ChoiceIn(BaseModel):
|
||||
text: str
|
||||
position: Optional[int]
|
||||
|
||||
|
||||
class QuestionIn(BaseModel):
|
||||
text: str
|
||||
type: Optional[str] = "single"
|
||||
position: Optional[int]
|
||||
choices: Optional[List[ChoiceIn]]
|
||||
|
||||
|
||||
class PollIn(BaseModel):
|
||||
title: str
|
||||
description: Optional[str]
|
||||
questions: Optional[List[QuestionIn]]
|
||||
|
||||
|
||||
def get_session():
|
||||
settings = Settings()
|
||||
engine = create_engine(settings.DATABASE_URL_syncpg, future=True)
|
||||
return sessionmaker(bind=engine, future=True)
|
||||
|
||||
|
||||
@router.post("/", status_code=201)
|
||||
def create_poll(payload: PollIn):
|
||||
Session = get_session()
|
||||
with Session() as session:
|
||||
poll = Poll(title=payload.title, description=payload.description)
|
||||
# polls are anonymous; no author_id stored
|
||||
session.add(poll)
|
||||
# questions and choices
|
||||
if payload.questions:
|
||||
for q in payload.questions:
|
||||
question = Question(text=q.text, type=q.type or "single", position=q.position, poll=poll)
|
||||
session.add(question)
|
||||
if q.choices:
|
||||
for idx, c in enumerate(q.choices):
|
||||
choice = Choice(text=c.text, position=c.position if c.position is not None else idx, question=question)
|
||||
session.add(choice)
|
||||
session.commit()
|
||||
session.refresh(poll)
|
||||
return {"id": str(poll.id)}
|
||||
|
||||
|
||||
@router.get("/{poll_id}")
|
||||
def get_poll(poll_id: str):
|
||||
Session = get_session()
|
||||
try:
|
||||
pid = uuid.UUID(poll_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
poll = session.get(Poll, pid)
|
||||
if not poll:
|
||||
raise HTTPException(status_code=404, detail="Poll not found")
|
||||
data = {
|
||||
"id": str(poll.id),
|
||||
"title": poll.title,
|
||||
"description": poll.description,
|
||||
"questions": [
|
||||
{
|
||||
"id": str(q.id),
|
||||
"text": q.text,
|
||||
"type": q.type,
|
||||
"choices": [{"id": str(c.id), "text": c.text} for c in q.choices]
|
||||
}
|
||||
for q in poll.questions
|
||||
],
|
||||
}
|
||||
return data
|
||||
|
||||
|
||||
@router.get("/")
|
||||
def list_polls():
|
||||
Session = get_session()
|
||||
with Session() as session:
|
||||
rows = session.query(Poll).all()
|
||||
return [{"id": str(p.id), "title": p.title, "description": p.description} for p in rows]
|
||||
118
route/question_crud.py
Normal file
118
route/question_crud.py
Normal file
@@ -0,0 +1,118 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
import uuid
|
||||
|
||||
from bd import Settings
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from bd.tables.question import Question
|
||||
from bd.tables.poll import Poll
|
||||
|
||||
router = APIRouter(tags=["questions"], prefix="/questions")
|
||||
|
||||
|
||||
class QuestionCreate(BaseModel):
|
||||
poll_id: str
|
||||
text: str
|
||||
type: Optional[str] = "single"
|
||||
position: Optional[int]
|
||||
|
||||
|
||||
class QuestionUpdate(BaseModel):
|
||||
text: Optional[str]
|
||||
type: Optional[str]
|
||||
position: Optional[int]
|
||||
|
||||
|
||||
def get_session():
|
||||
settings = Settings()
|
||||
engine = create_engine(settings.DATABASE_URL_syncpg, future=True)
|
||||
return sessionmaker(bind=engine, future=True)
|
||||
|
||||
|
||||
@router.post("/", status_code=201)
|
||||
def create_question(payload: QuestionCreate):
|
||||
Session = get_session()
|
||||
try:
|
||||
pid = uuid.UUID(payload.poll_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid poll UUID")
|
||||
with Session() as session:
|
||||
poll = session.get(Poll, pid)
|
||||
if not poll:
|
||||
raise HTTPException(status_code=404, detail="Poll not found")
|
||||
q = Question(poll_id=pid, text=payload.text, type=payload.type or "single", position=payload.position)
|
||||
session.add(q)
|
||||
session.commit()
|
||||
session.refresh(q)
|
||||
return {"id": str(q.id)}
|
||||
|
||||
|
||||
@router.put("/{question_id}")
|
||||
def update_question(question_id: str, payload: QuestionUpdate):
|
||||
Session = get_session()
|
||||
try:
|
||||
qid = uuid.UUID(question_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
q = session.get(Question, qid)
|
||||
if not q:
|
||||
raise HTTPException(status_code=404, detail="Question not found")
|
||||
if payload.text is not None:
|
||||
q.text = payload.text
|
||||
if payload.type is not None:
|
||||
q.type = payload.type
|
||||
if payload.position is not None:
|
||||
q.position = payload.position
|
||||
session.add(q)
|
||||
session.commit()
|
||||
session.refresh(q)
|
||||
return {"id": str(q.id)}
|
||||
|
||||
|
||||
@router.delete("/{question_id}", status_code=204)
|
||||
def delete_question(question_id: str):
|
||||
Session = get_session()
|
||||
try:
|
||||
qid = uuid.UUID(question_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
q = session.get(Question, qid)
|
||||
if not q:
|
||||
raise HTTPException(status_code=404, detail="Question not found")
|
||||
session.delete(q)
|
||||
session.commit()
|
||||
return {}
|
||||
|
||||
|
||||
@router.get("/")
|
||||
def list_questions(poll_id: Optional[str] = None):
|
||||
Session = get_session()
|
||||
with Session() as session:
|
||||
q = session.query(Question)
|
||||
if poll_id:
|
||||
try:
|
||||
pid = uuid.UUID(poll_id)
|
||||
q = q.filter(Question.poll_id == pid)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid poll_id UUID")
|
||||
rows = q.all()
|
||||
return [{"id": str(r.id), "text": r.text, "poll_id": str(r.poll_id)} for r in rows]
|
||||
|
||||
|
||||
@router.get("/{question_id}")
|
||||
def get_question(question_id: str):
|
||||
Session = get_session()
|
||||
try:
|
||||
qid = uuid.UUID(question_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
q = session.get(Question, qid)
|
||||
if not q:
|
||||
raise HTTPException(status_code=404, detail="Question not found")
|
||||
return {"id": str(q.id), "text": q.text, "poll_id": str(q.poll_id)}
|
||||
97
route/response_crud.py
Normal file
97
route/response_crud.py
Normal file
@@ -0,0 +1,97 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
import uuid
|
||||
|
||||
from bd import Settings
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from bd.tables.poll import Poll
|
||||
from bd.tables.question import Question
|
||||
from bd.tables.response import Response, Answer
|
||||
|
||||
router = APIRouter(tags=["responses"], prefix="/polls")
|
||||
|
||||
|
||||
class AnswerIn(BaseModel):
|
||||
question_id: str
|
||||
choice_id: Optional[str]
|
||||
text: Optional[str]
|
||||
|
||||
|
||||
class ResponseIn(BaseModel):
|
||||
user_id: Optional[str]
|
||||
answers: List[AnswerIn]
|
||||
|
||||
|
||||
def get_session():
|
||||
settings = Settings()
|
||||
engine = create_engine(settings.DATABASE_URL_syncpg, future=True)
|
||||
return sessionmaker(bind=engine, future=True)
|
||||
|
||||
|
||||
@router.post("/{poll_id}/responses", status_code=201)
|
||||
def submit_response(poll_id: str, payload: ResponseIn):
|
||||
Session = get_session()
|
||||
try:
|
||||
pid = uuid.UUID(poll_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid poll UUID")
|
||||
with Session() as session:
|
||||
poll = session.get(Poll, pid)
|
||||
if not poll:
|
||||
raise HTTPException(status_code=404, detail="Poll not found")
|
||||
resp = Response(poll_id=pid)
|
||||
if payload.user_id:
|
||||
try:
|
||||
resp.user_id = uuid.UUID(payload.user_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid user_id UUID")
|
||||
session.add(resp)
|
||||
# validate answers
|
||||
for a in payload.answers:
|
||||
try:
|
||||
qid = uuid.UUID(a.question_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid question_id UUID")
|
||||
question = session.get(Question, qid)
|
||||
if not question or question.poll_id != pid:
|
||||
raise HTTPException(status_code=400, detail="Question does not belong to poll")
|
||||
choice_uuid = None
|
||||
if a.choice_id:
|
||||
try:
|
||||
choice_uuid = uuid.UUID(a.choice_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid choice_id UUID")
|
||||
answer = Answer(response=resp, question_id=qid, choice_id=choice_uuid, text=a.text)
|
||||
session.add(answer)
|
||||
session.commit()
|
||||
session.refresh(resp)
|
||||
return {"id": str(resp.id)}
|
||||
|
||||
|
||||
@router.get("/{poll_id}/responses")
|
||||
def list_responses(poll_id: str):
|
||||
Session = get_session()
|
||||
try:
|
||||
pid = uuid.UUID(poll_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid poll UUID")
|
||||
with Session() as session:
|
||||
rows = session.query(Response).filter(Response.poll_id == pid).all()
|
||||
return [{"id": str(r.id), "user_id": str(r.user_id) if r.user_id else None, "submitted_at": r.submitted_at.isoformat()} for r in rows]
|
||||
|
||||
|
||||
@router.get("/responses/{response_id}")
|
||||
def get_response(response_id: str):
|
||||
Session = get_session()
|
||||
try:
|
||||
rid = uuid.UUID(response_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
r = session.get(Response, rid)
|
||||
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]}
|
||||
111
route/ui.py
Normal file
111
route/ui.py
Normal file
@@ -0,0 +1,111 @@
|
||||
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>
|
||||
"""
|
||||
98
route/users_crud.py
Normal file
98
route/users_crud.py
Normal file
@@ -0,0 +1,98 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
import uuid
|
||||
|
||||
from bd import Settings
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from bd.tables.users import User
|
||||
|
||||
router = APIRouter(tags=["users"], prefix="/users")
|
||||
|
||||
|
||||
class UserCreate(BaseModel):
|
||||
first_name: str
|
||||
last_name: str
|
||||
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
first_name: Optional[str]
|
||||
last_name: Optional[str]
|
||||
|
||||
|
||||
def get_session():
|
||||
settings = Settings()
|
||||
engine = create_engine(settings.DATABASE_URL_syncpg, future=True)
|
||||
return sessionmaker(bind=engine, future=True)
|
||||
|
||||
|
||||
@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)
|
||||
session.add(user)
|
||||
session.commit()
|
||||
session.refresh(user)
|
||||
return {"id": str(user.id), "first_name": user.first_name, "last_name": user.last_name}
|
||||
|
||||
|
||||
@router.put("/{user_id}")
|
||||
def update_user(user_id: str, payload: UserUpdate):
|
||||
Session = get_session()
|
||||
try:
|
||||
uid = uuid.UUID(user_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
user = session.get(User, uid)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
if payload.first_name is not None:
|
||||
user.first_name = payload.first_name
|
||||
if payload.last_name is not None:
|
||||
user.last_name = payload.last_name
|
||||
session.add(user)
|
||||
session.commit()
|
||||
session.refresh(user)
|
||||
return {"id": str(user.id), "first_name": user.first_name, "last_name": user.last_name}
|
||||
|
||||
|
||||
@router.delete("/{user_id}", status_code=204)
|
||||
def delete_user(user_id: str):
|
||||
Session = get_session()
|
||||
try:
|
||||
uid = uuid.UUID(user_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
user = session.get(User, uid)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
session.delete(user)
|
||||
session.commit()
|
||||
return {}
|
||||
|
||||
|
||||
@router.get("/")
|
||||
def list_users():
|
||||
Session = get_session()
|
||||
with Session() as session:
|
||||
rows = session.query(User).all()
|
||||
return [{"id": str(u.id), "first_name": u.first_name, "last_name": u.last_name} for u in rows]
|
||||
|
||||
|
||||
@router.get("/{user_id}")
|
||||
def get_user(user_id: str):
|
||||
Session = get_session()
|
||||
try:
|
||||
uid = uuid.UUID(user_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
user = session.get(User, uid)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return {"id": str(user.id), "first_name": user.first_name, "last_name": user.last_name}
|
||||
Reference in New Issue
Block a user