Autenticação e Gerenciamento de Usuários API
Autenticação e Gerenciamento de Usuários API
# Importações relativas
from . import models, schemas, crud
from .database import get_db
SECRET_KEY = "sua_chave_secreta_muito_segura_e_longa"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/token")
if not user:
[Link](f"Usuário com email '{username}' não encontrado.")
return None
crud
from [Link] import Session, joinedload
def get_questions(
db: Session,
skip: int = 0,
limit: int = 100,
materia: Optional[str] = None,
assunto: Optional[str] = None,
banca: Optional[str] = None,
orgao: Optional[str] = None,
cargo: Optional[str] = None,
ano: Optional[int] = None,
escolaridade: Optional[str] = None,
dificuldade: Optional[str] = None,
regiao: Optional[str] = None
) -> List[[Link]]:
"""
Lista questões com filtros opcionais.
"""
query = [Link]([Link])
if materia:
query = [Link]([Link] == materia)
if assunto:
query = [Link]([Link] == assunto)
if banca:
query = [Link]([Link] == banca)
if orgao:
query = [Link]([Link] == orgao)
if cargo:
query = [Link]([Link] == cargo)
if ano:
query = [Link]([Link] == ano)
if escolaridade:
query = [Link]([Link] == escolaridade)
if dificuldade:
query = [Link]([Link] == dificuldade)
if regiao:
query = [Link]([Link] == regiao)
return [Link](skip).limit(limit).all()
def count_questions(
db: Session,
materia: Optional[str] = None,
assunto: Optional[str] = None,
banca: Optional[str] = None,
orgao: Optional[str] = None,
cargo: Optional[str] = None,
ano: Optional[int] = None,
escolaridade: Optional[str] = None,
dificuldade: Optional[str] = None,
regiao: Optional[str] = None
) -> Dict[str, Any]:
"""
Conta o número de questões com base nos filtros e retorna seus IDs.
"""
query = [Link]([Link])
if materia:
query = [Link]([Link] == materia)
if assunto:
query = [Link]([Link] == assunto)
if banca:
query = [Link]([Link] == banca)
if orgao:
query = [Link]([Link] == orgao)
if cargo:
query = [Link]([Link] == cargo)
if ano:
query = [Link]([Link] == ano)
if escolaridade:
query = [Link]([Link] == escolaridade)
if dificuldade:
query = [Link]([Link] == dificuldade)
if regiao:
query = [Link]([Link] == regiao)
total_questoes_resolvidas = 0
total_acertos = 0
total_erros = 0
total_questoes_resolvidas += len(respostas_salvas)
if respostas_salvas:
question_ids_in_answers = [int(q_id) for q_id in
respostas_salvas.keys()]
questions_for_scoring = [Link]([Link],
[Link], [Link]).filter(
[Link].in_(question_ids_in_answers)
).all()
if tipo == "multipla":
correct_index = gabarito_map_multipla.get([Link]())
if user_answer is not None and user_answer == correct_index:
total_acertos += 1
elif tipo == "certo_errado":
correct_value = 1 if [Link]() == 'certo' else 0
if user_answer is not None and user_answer == correct_value:
total_acertos += 1
return [Link](
total_questoes_resolvidas=total_questoes_resolvidas,
total_acertos=total_acertos,
total_erros=total_erros,
percentual_acerto=round(percentual_acerto, 2)
)
if not db_notebook:
return None
alternativas = []
if q.item_a: [Link](q.item_a)
if q.item_b: [Link](q.item_b)
if q.item_c: [Link](q.item_c)
if q.item_d: [Link](q.item_d)
if q.item_e: [Link](q.item_e)
q_dict['alternativas'] = alternativas
correta_index = None
if [Link] == "multipla":
# Mapeia 'A'->0, 'B'->1, etc.
gabarito_map = {'A': 0, 'B': 1, 'C': 2, 'D': 3, 'E': 4}
correta_index = gabarito_map.get([Link]())
elif [Link] == "certo_errado":
# 'Certo' -> 1, 'Errado' -> 0
correta_index = 1 if [Link]() == 'certo' else (0 if
[Link]() == 'errado' else None)
q_dict['correta'] = correta_index
processed_questions.append([Link](**q_dict)) # Converte para
o schema Question
return db_notebook
acertos_count = 0
if respostas_salvas:
question_ids_in_answers = [int(q_id) for q_id in
respostas_salvas.keys()]
questions_for_scoring = [Link]([Link],
[Link], [Link]).filter(
[Link].in_(question_ids_in_answers)
).all()
if tipo == "multipla":
correct_index = gabarito_map_multipla.get([Link]())
if user_answer is not None and user_answer == correct_index:
acertos_count += 1
elif tipo == "certo_errado":
correct_value = 1 if [Link]() == 'certo' else 0
if user_answer is not None and user_answer == correct_value:
acertos_count += 1
[Link] = acertos_count
else:
[Link] = 0
[Link] = 0
return notebooks
[Link](db_notebook)
[Link]()
return True
return False
[Link](db_notebook)
[Link]()
[Link](db_notebook)
# Converte de volta de string JSON para objetos Python antes de retornar
db_notebook.questoes_ids = [Link](db_notebook.questoes_ids)
db_notebook.filtros = [Link](db_notebook.filtros)
return db_notebook
return None
if db_progress:
db_progress.index = progress_data.index
db_progress.respostas = [Link](progress_data.respostas) # Converte
dict para string JSON
else:
db_progress = [Link](
notebook_id=notebook_id,
user_id=user_id,
index=progress_data.index,
respostas=[Link](progress_data.respostas) # Converte dict para
string JSON
)
[Link](db_progress)
[Link]()
[Link](db_progress)
# Verifica se respostas é uma string JSON antes de tentar carregar
db_progress.respostas = [Link](db_progress.respostas) if
isinstance(db_progress.respostas, str) else db_progress.respostas
return db_progress
if order_by == "createdAt":
if order_direction == "asc":
query = query.order_by([Link].created_at.asc())
else:
query = query.order_by([Link].created_at.desc())
elif order_by == "points":
if order_direction == "asc":
query = query.order_by([Link]())
else:
query = query.order_by([Link]())
comments = [Link](joinedload([Link])).all() #
Carrega o autor junto
formatted_comments = []
for comment in comments:
comment_dict = comment.__dict__.copy()
# Garante que o 'user' seja um objeto UserForComment para o schema de
retorno
comment_dict['user'] = [Link](id=[Link],
username=[Link])
formatted_comments.append([Link](**comment_dict))
return formatted_comments
if comment.user_id == user_id:
raise HTTPException(status_code=403, detail="Você não pode votar no
próprio comentário.")
existing_vote = (
[Link]([Link])
.filter_by(comment_id=comment_id, user_id=user_id)
.first()
)
if existing_vote:
if existing_vote.vote_type == vote_type:
# Clicou novamente no mesmo tipo de voto → remover
[Link](existing_vote)
if vote_type == "upvote":
[Link] = max([Link] - 1, 0)
elif vote_type == "downvote":
[Link] += 1
else:
# Trocou de voto (ex: de positivo pra negativo)
if existing_vote.vote_type == "upvote" and vote_type == "downvote":
[Link] = max([Link] - 2, 0)
elif existing_vote.vote_type == "downvote" and vote_type == "upvote":
[Link] += 2
existing_vote.vote_type = vote_type
[Link](existing_vote)
else:
# Primeiro voto
new_vote = [Link](comment_id=comment_id, user_id=user_id,
vote_type=vote_type)
[Link](new_vote)
if vote_type == "upvote":
[Link] += 1
elif vote_type == "downvote":
[Link] = max([Link] - 1, 0)
[Link]()
[Link](comment)
return comment
votes = (
[Link]([Link])
.filter([Link].user_id == user_id)
.filter([Link].comment_id.in_([[Link] for c in comments]))
.all()
)
formatted_comments = []
for c in comments:
comment_dict = c.__dict__.copy()
comment_dict["user"] = [Link](
id=[Link],
username=[Link]
)
comment_data = [Link](**comment_dict)
formatted_comments.append({
**comment_data.model_dump(),
"voted_by_me": vote_map.get([Link])
})
return formatted_comments
if db_theory:
db_theory.content = theory_data.content
else:
db_theory = [Link](
materia=theory_data.materia,
assunto=theory_data.assunto,
content=theory_data.content
)
[Link](db_theory)
[Link]()
[Link](db_theory)
return db_theory
grouped_theories = {}
for materia, assunto in theories:
if materia not in grouped_theories:
grouped_theories[materia] = []
grouped_theories[materia].append(assunto)
result = []
for materia, assuntos in grouped_theories.items():
[Link]({"materia": materia, "assuntos": sorted(assuntos)})
return result
database
from sqlalchemy import create_engine
from [Link] import declarative_base
from [Link] import sessionmaker
# SQLALCHEMY_DATABASE_URL = "postgresql://user:password@postgresserver/db"
SQLALCHEMY_DATABASE_URL = "sqlite:///./[Link]" # Usando SQLite
engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} #
Necessário para SQLite
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def get_db():
db = SessionLocal()
try:
yield db
finally:
[Link]()
main
# Importações relativas
from . import crud, models, schemas, auth
from .database import SessionLocal, engine, get_db
app = FastAPI()
# Configuração do CORS
origins = [
"[Link] # Onde seu frontend React está rodando
"[Link]
# Adicione outros domínios se o frontend for hospedado em outro lugar
]
app.add_middleware(
CORSMiddleware,
allow_origins=["[Link] # frontend
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@[Link]("/api/token", response_model=[Link])
async def login_for_access_token(
form_data: OAuth2PasswordRequestForm = Depends(), db: Session =
Depends(get_db)
):
"""
Endpoint para autenticação de usuário e emissão de token JWT.
Espera 'username' (que é o email) e 'password'.
"""
user = auth.authenticate_user(db, form_data.username, form_data.password)
if not user:
[Link](f"Tentativa de login falhou para email:
{form_data.username}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Email ou senha incorretos",
headers={"WWW-Authenticate": "Bearer"},
)
if not user.is_active:
[Link](f"Usuário inativo tentou logar: {[Link]}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Sua conta está pendente de aprovação ou foi desativada. Entre
em contato com o administrador.",
)
@[Link]("/api/users/me/", response_model=[Link])
async def read_users_me(current_user: [Link] =
Depends(auth.get_current_user)):
"""
Endpoint para obter informações do usuário logado.
"""
[Link](f"Acessando informações do usuário logado:
{current_user.username}")
return current_user
@[Link]("/api/register/", response_model=[Link])
async def register_user(user: [Link], db: Session =
Depends(get_db)):
"""
Endpoint para registro de novos usuários.
"""
db_user_email = crud.get_user_by_email(db, email=[Link])
if db_user_email:
[Link](f"Tentativa de registro com email já existente:
{[Link]}")
raise HTTPException(status_code=400, detail="Email já registrado")
hashed_password = auth.get_password_hash([Link])
[Link] = hashed_password
@[Link]("/api/users/pending/", response_model=List[[Link]])
async def get_pending_users(current_user: [Link] =
Depends(auth.get_current_admin_user), db: Session = Depends(get_db)):
"""
Retorna uma lista de usuários pendentes de aprovação. Apenas para
administradores.
"""
[Link](f"Admin {current_user.username} buscando usuários pendentes.")
users = crud.get_pending_users(db)
return users
@[Link]("/api/users/{user_id}/approve", response_model=[Link])
async def approve_user(user_id: int, current_user: [Link] =
Depends(auth.get_current_admin_user), db: Session = Depends(get_db)):
"""
Aprova um usuário, ativando sua conta. Apenas para administradores.
"""
[Link](f"Admin {current_user.username} aprovando usuário ID:
{user_id}")
user = crud.update_user_status(db, user_id, is_active=True)
if not user:
[Link](f"Tentativa de aprovar usuário inexistente ID:
{user_id}")
raise HTTPException(status_code=404, detail="Usuário não encontrado")
return user
@[Link]("/api/users/{user_id}/reject", response_model=[Link])
async def reject_user(user_id: int, current_user: [Link] =
Depends(auth.get_current_admin_user), db: Session = Depends(get_db)):
"""
Rejeita um usuário, desativando sua conta. Apenas para administradores.
"""
[Link](f"Admin {current_user.username} rejeitando usuário ID:
{user_id}")
user = crud.update_user_status(db, user_id, is_active=False)
if not user:
[Link](f"Tentativa de rejeitar usuário inexistente ID:
{user_id}")
raise HTTPException(status_code=404, detail="Usuário não encontrado")
return user
@[Link]("/api/questions/", response_model=[Link],
status_code=status.HTTP_201_CREATED)
async def create_question(
question: [Link],
current_user: [Link] = Depends(auth.get_current_admin_user),
db: Session = Depends(get_db)
):
[Link](f"Admin {current_user.username} criando nova questão.")
db_question = crud.create_question(db=db, question=question)
return db_question
@[Link]("/api/questions/fields/{field_name}", response_model=List[str])
async def get_unique_fields(
field_name: str,
db: Session = Depends(get_db),
current_user: [Link] = Depends(auth.get_current_user)
):
try:
print(f"Campo solicitado: {field_name}")
valores = crud.get_unique_question_fields(db, field_name)
return [str(v) for v in valores if v is not None]
except Exception as e:
print(f"❌ Erro ao buscar campo '{field_name}':", e)
raise HTTPException(status_code=500, detail=str(e))
@[Link]("/api/questions/", response_model=List[[Link]])
async def read_questions(
skip: int = 0,
limit: int = 100,
materia: Optional[str] = None,
assunto: Optional[str] = None,
banca: Optional[str] = None,
orgao: Optional[str] = None,
cargo: Optional[str] = None,
ano: Optional[int] = None,
escolaridade: Optional[str] = None,
dificuldade: Optional[str] = None,
regiao: Optional[str] = None,
current_user: [Link] = Depends(auth.get_current_user),
db: Session = Depends(get_db)
):
[Link](f"Usuário {current_user.username} buscando questões com
filtros: Materia={materia}, Assunto={assunto}, Banca={banca}")
questions = crud.get_questions(
db, skip=skip, limit=limit,
materia=materia, assunto=assunto, banca=banca,
orgao=orgao, cargo=cargo, ano=ano,
escolaridade=escolaridade, dificuldade=dificuldade, regiao=regiao
)
return questions
@[Link]("/api/questions/{question_id}", response_model=[Link])
async def read_question(
question_id: int,
db: Session = Depends(get_db),
current_user: [Link] = Depends(auth.get_current_user)
):
[Link](f"Usuário {current_user.username} buscando questão ID:
{question_id}")
db_question = crud.get_question(db, question_id=question_id)
if db_question is None:
[Link](f"Questão ID {question_id} não encontrada.")
raise HTTPException(status_code=404, detail="Questão não encontrada")
return db_question
@[Link]("/api/questions/{question_id}", response_model=[Link])
async def update_question(
question_id: int,
question: [Link],
current_user: [Link] = Depends(auth.get_current_admin_user),
db: Session = Depends(get_db)
):
[Link](f"Admin {current_user.username} atualizando questão ID:
{question_id}")
db_question = crud.update_question(db, question_id=question_id,
question_update=question)
if db_question is None:
[Link](f"Tentativa de atualizar questão inexistente ID:
{question_id}")
raise HTTPException(status_code=404, detail="Questão não encontrada")
return db_question
@[Link]("/api/questions/{question_id}",
status_code=status.HTTP_204_NO_CONTENT)
async def delete_question(
question_id: int,
current_user: [Link] = Depends(auth.get_current_admin_user),
db: Session = Depends(get_db)
):
[Link](f"Admin {current_user.username} deletando questão ID:
{question_id}")
success = crud.delete_question(db, question_id=question_id)
if not success:
[Link](f"Tentativa de deletar questão inexistente ID:
{question_id}")
raise HTTPException(status_code=404, detail="Questão não encontrada")
return {"message": "Questão deletada com sucesso"}
@[Link]("/api/questions/{question_id}/statistics/split",
response_model=Dict[str, Any])
async def get_split_statistics(
question_id: int,
db: Session = Depends(get_db),
current_user: [Link] = Depends(auth.get_current_user)
):
total = crud.get_question_statistics(db, question_id)
user_attempts = 0
user_corrects = 0
user_progresses = [Link]([Link]).filter(
[Link].user_id == current_user.id
).all()
return {
"total": {
"tentativas": total.total_attempts,
"acertos": total.correct_attempts
},
"usuario": {
"tentativas": user_attempts,
"acertos": user_corrects
},
"outros": {
"tentativas": others_attempts,
"acertos": others_corrects
}
}
@[Link]("/api/questions/{question_id}/statistics",
response_model=[Link])
async def update_question_statistics_route(
question_id: int,
# CORREÇÃO AQUI: Use Body para indicar que 'is_correct' vem do corpo da
requisição JSON
is_correct: bool = Body(..., embed=True),
current_user: [Link] = Depends(auth.get_current_user),
db: Session = Depends(get_db)
):
"""
Atualiza as estatísticas de uma questão (total de tentativas, acertos).
Acessível por usuários e administradores.
"""
[Link](f"Usuário {current_user.username} atualizando estatísticas
para questão ID: {question_id}, Correta: {is_correct}")
stats = crud.update_question_statistics(db, question_id, is_correct)
if not stats:
[Link](f"Questão ID {question_id} não encontrada para atualizar
estatísticas.")
raise HTTPException(status_code=404, detail="Questão não encontrada para
atualizar estatísticas")
return stats
@[Link]("/api/users/me/stats", response_model=[Link])
async def get_my_stats(current_user: [Link] =
Depends(auth.get_current_user), db: Session = Depends(get_db)):
"""
Retorna as estatísticas gerais de estudo para o usuário logado.
"""
[Link](f"Usuário {current_user.username} buscando estatísticas
gerais.")
stats = crud.get_user_overall_stats(db, current_user.id)
return stats
@[Link]("/api/notebooks/", response_model=[Link],
status_code=status.HTTP_201_CREATED)
async def create_notebook(
notebook: [Link],
current_user: [Link] = Depends(auth.get_current_user),
db: Session = Depends(get_db)
):
"""
Cria um novo caderno para o usuário logado.
"""
[Link](f"Usuário {current_user.username} criando caderno:
{[Link]}")
db_notebook = crud.create_notebook(db=db, notebook=notebook,
user_id=current_user.id)
return db_notebook
@[Link]("/api/notebooks/", response_model=List[[Link]])
async def read_user_notebooks(
current_user: [Link] = Depends(auth.get_current_user),
db: Session = Depends(get_db)
):
"""
Retorna todos os cadernos do usuário logado.
"""
[Link](f"Usuário {current_user.username} buscando seus cadernos.")
notebooks = crud.get_user_notebooks(db, user_id=current_user.id)
return notebooks
@[Link]("/api/notebooks/{notebook_id}/resolve_data",
response_model=[Link])
async def get_notebook_resolve_data(
notebook_id: int,
current_user: [Link] = Depends(auth.get_current_user),
db: Session = Depends(get_db)
):
"""
Retorna os dados completos de um caderno para resolução, incluindo
questões e progresso.
"""
[Link](f"Usuário {current_user.username} buscando dados para resolver
caderno ID: {notebook_id}")
db_notebook_data = crud.get_notebook_with_questions(db, notebook_id,
current_user.id)
if not db_notebook_data:
[Link](f"Caderno ID {notebook_id} não encontrado para o usuário
{current_user.username}.")
raise HTTPException(status_code=404, detail="Caderno não encontrado ou
você não tem permissão para acessá-lo.")
progress_schema = None
if db_notebook_data.progress_data:
progress_schema =
[Link].model_validate(db_notebook_data.progress_data)
return [Link](
nome=db_notebook_data.nome,
questoes=db_notebook_data.questions_data,
progresso=progress_schema
)
@[Link]("/api/notebooks/{notebook_id}/progress",
response_model=[Link])
async def update_progress(
notebook_id: int,
progress_data: [Link],
current_user: [Link] = Depends(auth.get_current_user),
db: Session = Depends(get_db)
):
"""
Atualiza o progresso de um caderno para o usuário logado.
"""
[Link](f"Usuário {current_user.username} atualizando progresso para
caderno ID: {notebook_id}")
db_progress = crud.create_or_update_notebook_progress(db, notebook_id,
current_user.id, progress_data)
return db_progress
@[Link]("/api/notebooks/{notebook_id}/resposta")
async def register_question_response(
notebook_id: int,
response_data: Dict[str, Any],
db: Session = Depends(get_db),
current_user: [Link] = Depends(auth.get_current_user)
):
"""
Registra a resposta de uma questão e atualiza as estatísticas da questão.
"""
question_id = response_data.get("questaoId")
is_correct = response_data.get("acertou")
try:
crud.update_question_statistics(db, question_id=question_id,
is_correct=is_correct)
return {"message": "Resposta registrada e estatísticas atualizadas com
sucesso."}
except Exception as e:
[Link](f"Erro ao registrar resposta e atualizar estatísticas para
questão ID {question_id}: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Erro interno ao processar
a resposta: {e}")
@[Link]("/api/notebooks/{notebook_id}", response_model=[Link])
async def update_notebook(
notebook_id: int,
notebook_update: [Link],
current_user: [Link] = Depends(auth.get_current_user),
db: Session = Depends(get_db)
):
"""
Atualiza o nome de um caderno.
"""
[Link](f"Usuário {current_user.username} atualizando caderno ID:
{notebook_id}")
db_notebook = crud.update_notebook(db, notebook_id=notebook_id,
notebook_update=notebook_update)
if not db_notebook:
[Link](f"Caderno ID {notebook_id} não encontrado para
atualização pelo usuário {current_user.username}.")
raise HTTPException(status_code=404, detail="Caderno não encontrado")
return db_notebook
@[Link]("/api/notebooks/{notebook_id}",
status_code=status.HTTP_204_NO_CONTENT)
async def delete_notebook(
notebook_id: int,
current_user: [Link] = Depends(auth.get_current_user),
db: Session = Depends(get_db)
):
"""
Deleta um caderno.
"""
[Link](f"Usuário {current_user.username} deletando caderno ID:
{notebook_id}")
success = crud.delete_notebook(db, notebook_id=notebook_id)
if not success:
[Link](f"Caderno ID {notebook_id} não encontrado para exclusão
pelo usuário {current_user.username}.")
raise HTTPException(status_code=404, detail="Caderno não encontrado")
return {"message": "Caderno deletado com sucesso"}
@[Link]("/api/questions/{question_id}/comments",
response_model=[Link], status_code=status.HTTP_201_CREATED)
async def create_comment_for_question(
question_id: int,
comment: [Link],
current_user: [Link] = Depends(auth.get_current_user),
db: Session = Depends(get_db)
):
"""
Adiciona um comentário a uma questão específica.
"""
[Link](f"Usuário {current_user.username} adicionando comentário à
questão ID: {question_id}")
# Cria o comentário no banco
db_comment = crud.create_comment(db, question_id, current_user.id,
[Link])
@[Link]("/api/questions/{question_id}/comments",
response_model=List[Dict[str, Any]])
async def get_question_comments(
question_id: int,
orderBy: str = "createdAt", # ainda pode usar se quiser implementar no
futuro
order: str = "desc",
current_user: [Link] = Depends(auth.get_current_user),
db: Session = Depends(get_db)
):
"""
Retorna todos os comentários para uma questão específica, com status de
voto do usuário atual.
"""
[Link](f"Usuário {current_user.username} buscando comentários para
questão ID: {question_id}")
return crud.get_comments_with_vote_status(db, question_id,
current_user.id)
@[Link]("/api/questions/comments/{comment_id}",
response_model=[Link])
async def update_comment_route(
comment_id: int,
comment_update: [Link],
current_user: [Link] = Depends(auth.get_current_user),
db: Session = Depends(get_db)
):
"""
Atualiza um comentário existente.
"""
[Link](f"Usuário {current_user.username} atualizando comentário ID:
{comment_id}")
db_comment = crud.update_comment(db, comment_id, current_user.id,
comment_update.content)
if not db_comment:
[Link](f"Comentário ID {comment_id} não encontrado ou usuário
{current_user.username} não autorizado para atualizar.")
raise HTTPException(status_code=404, detail="Comentário não encontrado
ou você não tem permissão para editar.")
return db_comment
@[Link]("/api/questions/comments/{comment_id}",
status_code=status.HTTP_204_NO_CONTENT)
async def delete_comment_route(
comment_id: int,
current_user: [Link] = Depends(auth.get_current_user),
db: Session = Depends(get_db)
):
"""
Deleta um comentário.
"""
[Link](f"Usuário {current_user.username} deletando comentário ID:
{comment_id}")
success = crud.delete_comment(db, comment_id, current_user.id)
if not success:
[Link](f"Comentário ID {comment_id} não encontrado ou usuário
{current_user.username} não autorizado para deletar.")
raise HTTPException(status_code=404, detail="Comentário não encontrado
ou você não tem permissão para deletar.")
return {"message": "Comentário deletado com sucesso"}
@[Link]("/api/questions/comments/{comment_id}/vote",
response_model=[Link])
async def vote_on_comment(
comment_id: int,
type: str = Query(..., description="Tipo de voto: upvote, downvote ou
remove"),
current_user: [Link] = Depends(auth.get_current_user),
db: Session = Depends(get_db)
):
"""
Registra ou remove um voto em um comentário.
"""
[Link](f"Usuário {current_user.username} votando '{type}' no
comentário ID: {comment_id}")
if not db_comment:
[Link](f"Comentário ID {comment_id} não encontrado para
votação.")
raise HTTPException(status_code=404, detail="Comentário não
encontrado.")
return db_comment
@[Link]("/api/notes/{question_id}",
response_model=[Link])
def get_note(question_id: int, db: Session = Depends(get_db), current_user:
[Link] = Depends(auth.get_current_user)):
note = crud.get_note_by_user_and_question(db, current_user.id,
question_id)
if not note:
raise HTTPException(status_code=404, detail="Nota não encontrada")
return note
@[Link]("/api/notes/", response_model=[Link])
def create_or_update_note(note: [Link], db: Session =
Depends(get_db), current_user: [Link] =
Depends(auth.get_current_user)):
return crud.create_or_update_note(db, current_user.id, note)
@[Link]("/api/notes/{note_id}")
def delete_note(note_id: int, db: Session = Depends(get_db), current_user:
[Link] = Depends(auth.get_current_user)):
note = [Link]([Link]).filter_by(id=note_id,
user_id=current_user.id).first()
if not note:
raise HTTPException(status_code=404, detail="Nota não encontrada")
[Link](note)
[Link]()
return {"ok": True}
@[Link]("/api/theories/", response_model=[Link],
status_code=status.HTTP_201_CREATED)
async def create_or_update_theory(
theory: [Link],
current_user: [Link] = Depends(auth.get_current_admin_user),
db: Session = Depends(get_db)
):
"""
Cria ou atualiza uma teoria. Apenas para administradores.
"""
[Link](f"Admin {current_user.username} criando/atualizando teoria
para Matéria: {[Link]}, Assunto: {[Link]}")
db_theory = crud.create_or_update_theory(db, theory)
return db_theory
@[Link]("/api/theories/", response_model=List[[Link]])
async def get_all_theory_metadata(
current_user: [Link] = Depends(auth.get_current_user),
db: Session = Depends(get_db)
):
"""
Retorna metadados (matéria e assunto) de todas as teorias.
Acessível por usuários e administradores.
"""
[Link](f"Usuário {current_user.username} buscando metadados de
teorias.")
theories_meta = crud.get_all_theory_metadata(db)
return theories_meta
@[Link]("/api/theories/{materia}/{assunto}", response_model=[Link])
async def get_theory_content(
materia: str,
assunto: str,
current_user: [Link] = Depends(auth.get_current_user),
db: Session = Depends(get_db)
):
"""
Retorna o conteúdo de uma teoria específica por matéria e assunto.
Acessível por usuários e administradores.
"""
[Link](f"Usuário {current_user.username} buscando teoria para
Matéria: {materia}, Assunto: {assunto}")
db_theory = crud.get_theory(db, materia, assunto)
if not db_theory:
[Link](f"Teoria não encontrada para Matéria: {materia}, Assunto:
{assunto}")
raise HTTPException(status_code=404, detail="Teoria não encontrada")
return db_theory
@[Link]("/api/theories/{materia}/{assunto}",
status_code=status.HTTP_204_NO_CONTENT)
async def delete_theory_route(
materia: str,
assunto: str,
current_user: [Link] = Depends(auth.get_current_admin_user),
db: Session = Depends(get_db)
):
"""
Deleta uma teoria específica. Apenas para administradores.
"""
[Link](f"Admin {current_user.username} deletando teoria para Matéria:
{materia}, Assunto: {assunto}")
success = crud.delete_theory(db, materia, assunto)
if not success:
[Link](f"Teoria não encontrada para exclusão: Materia={materia},
Assunto={assunto}")
raise HTTPException(status_code=404, detail="Teoria não encontrada")
return {"message": "Teoria deletada com sucesso"}
@[Link]("/api/upload-pdf/")
async def upload_pdf(
file: UploadFile = File(...),
current_user: [Link] = Depends(auth.get_current_admin_user),
db: Session = Depends(get_db)
):
"""
Endpoint para upload e processamento de arquivos PDF para extração de
questões.
Apenas para administradores.
"""
[Link](f"Admin {current_user.username} iniciando upload de PDF:
{[Link]}")
if not [Link](".pdf"):
[Link](f"Tentativa de upload de arquivo não PDF:
{[Link]}")
raise HTTPException(status_code=400, detail="Apenas arquivos PDF são
permitidos.")
try:
pdf_content = await [Link]()
num_questions_added = pdf_processor.process_pdf_and_add_questions(db,
pdf_content)
Models
from [Link] import relationship
from [Link] import declarative_base
from [Link] import func
from sqlalchemy import UniqueConstraint
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey,
DateTime, Text, JSON
import json # Importa json para lidar com campos JSON
Base = declarative_base()
class User(Base):
__tablename__ = "users"
# ✅ Relacionamentos necessários:
notebooks = relationship("Notebook", back_populates="owner")
comments = relationship("Comment", back_populates="author")
progresses = relationship("NotebookProgress", back_populates="user_rel")
def __repr__(self):
return f"<User(id={[Link]}, username='{[Link]}',
role='{[Link]}')>"
class Question(Base):
"""
Modelo de Questão de Prova.
"""
__tablename__ = "questions"
# Relacionamentos
comments = relationship("Comment", back_populates="question")
question_stats = relationship("QuestionStatistics",
back_populates="question", uselist=False) # Um para um
def __repr__(self):
return f"<Question(id={[Link]}, materia='{[Link]}',
assunto='{[Link]}')>"
class QuestionStatistics(Base):
"""
Modelo para armazenar estatísticas de desempenho de uma questão.
"""
__tablename__ = "question_statistics"
# Relacionamento
question = relationship("Question", back_populates="question_stats")
def __repr__(self):
return f"<QuestionStatistics(question_id={self.question_id},
total={self.total_attempts}, correct={self.correct_attempts})>"
class Theory(Base):
"""
Modelo para armazenar conteúdo teórico.
"""
__tablename__ = "theories"
def __repr__(self):
return f"<Theory(id={[Link]}, materia='{[Link]}',
assunto='{[Link]}')>"
class Notebook(Base):
"""
Modelo de Caderno de Questões criado por um usuário.
"""
__tablename__ = "notebooks"
# Armazena os filtros usados para criar o caderno como uma string JSON
# Ex: '{"materia": "Direito Penal", "banca": "Cebraspe"}'
filtros = Column(Text, nullable=False, default="{}")
# Relacionamentos
owner = relationship("User", back_populates="notebooks")
children = relationship("Notebook", backref="parent", remote_side=[id]) #
Para pastas
# progress = relationship("NotebookProgress",
back_populates="notebook_rel", uselist=False) # Comentado na versão anterior
def __repr__(self):
return f"<Notebook(id={[Link]}, nome='{[Link]}',
user_id={self.user_id})>"
class NotebookProgress(Base):
"""
Modelo para armazenar o progresso do usuário em um caderno.
"""
__tablename__ = "notebook_progress"
def __repr__(self):
return f"<NotebookProgress(id={[Link]}, notebook_id={self.notebook_id},
user_id={self.user_id}, index={[Link]})>"
class Comment(Base):
__tablename__ = "comments"
# Relacionamentos
question = relationship("Question", back_populates="comments")
author = relationship("User", back_populates="comments")
def __repr__(self):
return f"<Comment(id={[Link]}, question_id={self.question_id},
user_id={self.user_id})>"
__table_args__ = (
UniqueConstraint("comment_id", "user_id",
name="unique_user_comment_vote"),
)
def __repr__(self):
return f"<CommentVote(comment_id={self.comment_id},
user_id={self.user_id}, vote_type={self.vote_type})>"
class QuestionNote(Base):
__tablename__ = "question_notes"
user = relationship("User")
question = relationship("Question")
pdf_parser
colocar o código
schemas
from pydantic import BaseModel, EmailStr, Field
from typing import List, Optional, Dict, Any
from datetime import datetime
class Token(BaseModel):
"""Schema para o token de acesso JWT."""
access_token: str
token_type: str
class TokenData(BaseModel):
"""Schema para os dados contidos no token JWT."""
username: Optional[str] = None
class UserBase(BaseModel):
"""Schema base para o usuário."""
email: EmailStr
username: str
class UserCreate(UserBase):
"""Schema para criação de um novo usuário."""
password: str
role: Optional[str] = "comum" # Define o papel padrão como "comum"
class User(UserBase):
"""Schema para representação completa do usuário (retorno da API)."""
id: int
is_active: bool
role: str
class Config:
from_attributes = True # Permite mapeamento de ORM (SQLAlchemy)
class UserForComment(BaseModel):
"""Schema simplificado para exibir informações do usuário em
comentários."""
id: int
username: str
class Config:
from_attributes = True
class UserStats(BaseModel):
"""Schema para as estatísticas gerais do usuário."""
total_questoes_resolvidas: int
total_acertos: int
total_erros: int
percentual_acerto: float
class QuestionBase(BaseModel):
"""Schema base para uma questão."""
enunciado: str
item_a: Optional[str] = None
item_b: Optional[str] = None
item_c: Optional[str] = None
item_d: Optional[str] = None
item_e: Optional[str] = None
materia: str
assunto: str
banca: str
orgao: Optional[str] = None
cargo: Optional[str] = None
ano: Optional[int] = None
escolaridade: Optional[str] = None
dificuldade: Optional[str] = None
regiao: Optional[str] = None
gabarito: str
informacoes: Optional[str] = None
comentarioProfessor: Optional[str] = None
tipo: str = "multipla" # "multipla" ou "certo_errado"
class QuestionCreate(QuestionBase):
"""Schema para criação de uma questão."""
pass
class Question(QuestionBase):
"""Schema para representação completa da questão (retorno da API)."""
id: int
alternativas: Optional[List[str]] = None # Campo adicionado para o
frontend, não no DB
correta: Optional[int] = None # Campo adicionado para o frontend, não no
DB
class Config:
from_attributes = True
class QuestionStatistics(BaseModel):
"""Schema para estatísticas de uma questão."""
question_id: int
total_attempts: int
correct_attempts: int
class Config:
from_attributes = True
class NotebookBase(BaseModel):
"""Schema base para um caderno."""
nome: str
questoes_ids: List[int] = Field(default_factory=list) # Lista de IDs de
questões
filtros: Dict[str, Any] = Field(default_factory=dict) # Filtros usados
para criar o caderno
paiId: Optional[int] = None # Para cadernos dentro de pastas
class NotebookCreate(NotebookBase):
"""Schema para criação de um caderno."""
pass
class NotebookUpdate(BaseModel):
"""Schema para atualização de um caderno (apenas nome)."""
nome: str
class Notebook(NotebookBase):
"""Schema para representação completa do caderno (retorno da API)."""
id: int
user_id: int
total_questoes: Optional[int] = None # Adicionado para exibir no frontend
respondidas: Optional[int] = None # Adicionado para exibir no frontend
acertos: Optional[int] = None # Adicionado para exibir no frontend
class Config:
from_attributes = True
class NotebookProgress(BaseModel):
"""Schema para o progresso do usuário em um caderno."""
id: int
notebook_id: int
user_id: int
index: int
respostas: Dict[str, int] # {question_id: user_answer_index}
updated_at: Optional[datetime] = None # Alterado para Optional[datetime] =
None
class Config:
from_attributes = True # Importante para o Pydantic validar de modelos
ORM
class NotebookProgressUpdate(BaseModel):
"""Schema para atualização do progresso do usuário em um caderno."""
index: int
respostas: Dict[str, int]
class NotebookResolveData(BaseModel):
"""Schema para os dados de resolução de um caderno (para o frontend)."""
nome: str
questoes: List[Question]
progresso: Optional[NotebookProgress] = None # O progresso pode não
existir ainda
class CommentCreate(BaseModel):
"""Schema para criação de um comentário."""
content: str
class Comment(BaseModel):
"""Schema para representação completa de um comentário."""
id: int
question_id: int
user_id: int
content: str
points: int
created_at: datetime
updated_at: Optional[datetime] = None
user: Optional[UserForComment] = None # ✅ Torna opcional
class Config:
from_attributes = True
class QuestionNoteBase(BaseModel):
question_id: int
content: str
class QuestionNoteCreate(QuestionNoteBase):
pass
class QuestionNoteResponse(QuestionNoteBase):
id: int
user_id: int
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
class TheoryCreate(BaseModel):
"""Schema para criação/atualização de uma teoria."""
materia: str
assunto: str
content: str
class Theory(BaseModel):
"""Schema para representação completa de uma teoria."""
id: int
materia: str
assunto: str
content: str
class Config:
from_attributes = True
class TheoryMeta(BaseModel):
"""Schema para metadados de teoria (matéria e assuntos)."""
materia: str
assuntos: List[str]
pdf_processor
logger = [Link](__name__)