0% acharam este documento útil (0 voto)
6 visualizações46 páginas

Autenticação e Gerenciamento de Usuários API

O documento apresenta um sistema de autenticação e gerenciamento de usuários utilizando FastAPI, SQLAlchemy e JWT. Ele inclui funções para autenticar usuários, criar e gerenciar tokens de acesso, além de operações CRUD para usuários e questões. O sistema também registra logs para monitorar atividades e erros durante o processo de autenticação e manipulação de dados.

Enviado por

e.ftomaz
Direitos autorais
© All Rights Reserved
Levamos muito a sério os direitos de conteúdo. Se você suspeita que este conteúdo é seu, reivindique-o aqui.
Formatos disponíveis
Baixe no formato ODT, PDF, TXT ou leia on-line no Scribd
0% acharam este documento útil (0 voto)
6 visualizações46 páginas

Autenticação e Gerenciamento de Usuários API

O documento apresenta um sistema de autenticação e gerenciamento de usuários utilizando FastAPI, SQLAlchemy e JWT. Ele inclui funções para autenticar usuários, criar e gerenciar tokens de acesso, além de operações CRUD para usuários e questões. O sistema também registra logs para monitorar atividades e erros durante o processo de autenticação e manipulação de dados.

Enviado por

e.ftomaz
Direitos autorais
© All Rights Reserved
Levamos muito a sério os direitos de conteúdo. Se você suspeita que este conteúdo é seu, reivindique-o aqui.
Formatos disponíveis
Baixe no formato ODT, PDF, TXT ou leia on-line no Scribd

auth

from datetime import datetime, timedelta


from typing import Optional
from jose import JWTError, jwt
from [Link] import CryptContext
from fastapi import Depends, HTTPException, status
from [Link] import OAuth2PasswordBearer
from [Link] import Session
import logging

# Configura o logger para exibir mensagens INFO ou DEBUG


[Link](level=[Link])
logger = [Link](__name__)

# Importações relativas
from . import models, schemas, crud
from .database import get_db

# Tente importar bcrypt diretamente para verificar a versão


try:
import bcrypt
except ImportError:
print("Aviso: A biblioteca 'bcrypt' não foi encontrada. Certifique-se de
que 'passlib[bcrypt]' está instalada.")
print("Para instalar: pip install 'passlib[bcrypt]'")

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

SECRET_KEY = "sua_chave_secreta_muito_segura_e_longa"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/token")

def verify_password(plain_password: str, hashed_password: str) -> bool:


"""
Verifica se uma senha em texto puro corresponde a uma senha hasheada.
"""
[Link](f"Verificando senha: plain_password
(parcial)={plain_password[:3]}..., hashed_password
(parcial)={hashed_password[:10]}...")
result = pwd_context.verify(plain_password, hashed_password)
[Link](f"Resultado da verificação: {result}")
return result

def get_password_hash(password: str) -> str:


"""
Gera o hash de uma senha em texto puro.
"""
hashed = pwd_context.hash(password)
[Link](f"Senha hasheada (parcial): {hashed[:10]}...")
return hashed

# FUNÇÃO AUTHENTICATE_USER AGORA BUSCA POR EMAIL


def authenticate_user(db: Session, username: str, password: str):
"""
Autentica um usuário verificando o email (passado como 'username' no
formulário) e a senha.
"""
[Link](f"Tentando autenticar usuário com email: {username}")
# AQUI ESTÁ A MUDANÇA: Usamos get_user_by_email para buscar pelo email
user = crud.get_user_by_email(db, email=username)

if not user:
[Link](f"Usuário com email '{username}' não encontrado.")
return None

[Link](f"Usuário '{username}' encontrado. Verificando senha...")


if not verify_password(password, user.hashed_password):
[Link](f"Senha incorreta para o usuário '{username}'.")
return None

[Link](f"Usuário '{username}' autenticado com sucesso.")


return user

def create_access_token(data: dict, expires_delta: Optional[timedelta] =


None):
"""
Cria um token de acesso JWT.
"""
to_encode = [Link]()
if expires_delta:
expire = [Link]() + expires_delta
else:
expire = [Link]() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = [Link](to_encode, SECRET_KEY, algorithm=ALGORITHM)
[Link](f"Token de acesso criado para sub: {[Link]('sub')}")
return encoded_jwt

async def get_current_user(token: str = Depends(oauth2_scheme), db: Session


= Depends(get_db)):
"""
Obtém o usuário atual a partir do token JWT.
"""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Não foi possível validar as credenciais",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = [Link](token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = [Link]("sub")
user_id: int = [Link]("id")
if username is None or user_id is None:
[Link]("Token sem username ou user_id.")
raise credentials_exception
token_data = [Link](username=username)
except JWTError as e:
[Link](f"Erro ao decodificar token JWT: {e}")
raise credentials_exception
user = crud.get_user_by_username(db, username=username) # A busca aqui
continua por username (sub do token)
if user is None or not user.is_active:
[Link](f"Usuário '{username}' não encontrado ou inativo para
token válido.")
raise credentials_exception
[Link](f"Usuário '{username}' obtido do token com sucesso.")
return [Link](id=[Link], username=[Link], email=[Link],
is_active=user.is_active, role=[Link])

async def get_current_admin_user(current_user: [Link] =


Depends(get_current_user)):
"""
Obtém o usuário atual e verifica se ele é um administrador.
"""
if current_user.role != "admin":
[Link](f"Tentativa de acesso admin por usuário não admin:
{current_user.username}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Operação não permitida para usuários não administradores",
)
[Link](f"Usuário admin '{current_user.username}' autenticado para
acesso admin.")
return current_user

crud
from [Link] import Session, joinedload

from . import models, schemas


from typing import List, Optional, Dict, Any
from sqlalchemy import distinct, func
import json # Importa json para lidar com campos JSON

# --- Funções CRUD para Usuários ---

def get_user(db: Session, user_id: int):


"""
Obtém um usuário pelo ID.
"""
return [Link]([Link]).filter([Link] == user_id).first()

def get_user_by_email(db: Session, email: str):


"""
Obtém um usuário pelo email.
"""
return [Link]([Link]).filter([Link] == email).first()

def get_user_by_username(db: Session, username: str):


"""
Obtém um usuário pelo nome de usuário.
"""
return [Link]([Link]).filter([Link] ==
username).first()

def create_user(db: Session, user: [Link]):


"""
Cria um novo usuário no banco de dados.
"""
db_user = [Link](
username=[Link],
email=[Link],
hashed_password=[Link], # A senha já deve vir hasheada do [Link]
role=[Link] if [Link] else "comum",
is_active=True if [Link] == "admin" else False # Admins ativos por
padrão, comuns pendentes
)
[Link](db_user)
[Link]()
[Link](db_user)
return db_user

def get_pending_users(db: Session) -> List[[Link]]:


"""
Retorna uma lista de usuários com role 'comum' que ainda não foram
ativados (is_active=False).
"""
return [Link]([Link]).filter(
[Link] == "comum",
[Link].is_active == False
).all()

def update_user_status(db: Session, user_id: int, is_active: bool):


"""
Atualiza o status de atividade de um usuário.
"""
db_user = [Link]([Link]).filter([Link] == user_id).first()
if db_user:
db_user.is_active = is_active
[Link](db_user)
[Link]()
[Link](db_user)
return db_user
return None

# --- Funções CRUD para Questões ---

def get_question(db: Session, question_id: int):


"""
Obtém uma questão pelo ID.
"""
return [Link]([Link]).filter([Link] ==
question_id).first()

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)

ids = [[Link] for q in [Link]()]


return {"count": len(ids), "ids": ids}

def create_question(db: Session, question: [Link]):


"""
Cria uma nova questão no banco de dados.
"""
db_question = [Link](
enunciado=[Link],
item_a=question.item_a,
item_b=question.item_b,
item_c=question.item_c,
item_d=question.item_d,
item_e=question.item_e,
materia=[Link],
assunto=[Link],
banca=[Link],
orgao=[Link],
cargo=[Link],
ano=[Link],
escolaridade=[Link],
dificuldade=[Link],
regiao=[Link],
gabarito=[Link],
informacoes=[Link],
comentarioProfessor=[Link],
tipo=[Link]
)
[Link](db_question)
[Link]()
[Link](db_question)
return db_question

def update_question(db: Session, question_id: int, question_update:


[Link]):
"""
Atualiza uma questão existente pelo ID.
"""
db_question = [Link]([Link]).filter([Link] ==
question_id).first()
if db_question:
for key, value in question_update.dict(exclude_unset=True).items():
setattr(db_question, key, value)
[Link](db_question)
[Link]()
[Link](db_question)
return db_question
return None

def delete_question(db: Session, question_id: int):


"""
Deleta uma questão pelo ID.
"""
db_question = [Link]([Link]).filter([Link] ==
question_id).first()
if db_question:
[Link](db_question)
[Link]()
return True
return False

def get_unique_question_fields(db: Session, field_name: str) -> List[str]:


"""
Retorna uma lista de valores únicos para um campo específico da tabela de
questões.
"""
if not hasattr([Link], field_name):
return []

query = [Link](distinct(getattr([Link], field_name)))


results = [Link]()
return sorted([r[0] for r in results if r[0] is not None])

def get_question_statistics(db: Session, question_id: int):


"""
Obtém ou cria estatísticas para uma questão.
"""
stats =
[Link]([Link]).filter([Link]
n_id == question_id).first()
if not stats:
stats = [Link](question_id=question_id)
[Link](stats)
[Link]()
[Link](stats)
return stats

def update_question_statistics(db: Session, question_id: int, is_correct:


bool):
"""
Atualiza as estatísticas de uma questão.
"""
stats = get_question_statistics(db, question_id) # Garante que as
estatísticas existam
stats.total_attempts += 1
if is_correct:
stats.correct_attempts += 1
[Link](stats)
[Link]()
[Link](stats)
return stats

# --- NOVA FUNÇÃO: Estatísticas do Usuário ---


def get_user_overall_stats(db: Session, user_id: int) -> [Link]:
"""
Calcula e retorna as estatísticas gerais de estudo para um usuário.
Isso agrega dados de todos os progressos de cadernos do usuário.
"""
all_user_progresses = [Link]([Link]).filter(
[Link].user_id == user_id
).all()

total_questoes_resolvidas = 0
total_acertos = 0
total_erros = 0

for progress in all_user_progresses:


# Verifica se respostas é uma string JSON antes de tentar carregar
respostas_salvas = [Link]([Link]) if
isinstance([Link], str) else [Link]

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()

gabarito_map_multipla = {'A': 0, 'B': 1, 'C': 2, 'D': 3, 'E': 4}

for q_id, gabarito, tipo in questions_for_scoring:


user_answer = respostas_salvas.get(str(q_id))

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

total_erros = total_questoes_resolvidas - total_acertos


percentual_acerto = (total_acertos / total_questoes_resolvidas) * 100 if
total_questoes_resolvidas > 0 else 0.0

return [Link](
total_questoes_resolvidas=total_questoes_resolvidas,
total_acertos=total_acertos,
total_erros=total_erros,
percentual_acerto=round(percentual_acerto, 2)
)

# --- Funções CRUD para Cadernos ---

def create_notebook(db: Session, notebook: [Link], user_id:


int):
"""
Cria um novo caderno para um usuário.
"""
db_notebook = [Link](
nome=[Link],
user_id=user_id,
questoes_ids=[Link](notebook.questoes_ids), # Converte lista para
string JSON
filtros=[Link]([Link]), # Converte dict para string JSON
paiId=[Link]
)
[Link](db_notebook)
[Link]()
[Link](db_notebook)
# Desserializa antes de retornar para corresponder ao schema
db_notebook.questoes_ids = [Link](db_notebook.questoes_ids)
db_notebook.filtros = [Link](db_notebook.filtros)
return db_notebook

def get_notebook(db: Session, notebook_id: int):


"""
Obtém um caderno pelo ID.
"""
db_notebook = [Link]([Link]).filter([Link] ==
notebook_id).first()
if db_notebook:
# Converte de volta de string JSON para objetos Python
db_notebook.questoes_ids = [Link](db_notebook.questoes_ids)
db_notebook.filtros = [Link](db_notebook.filtros)
[Link](db_notebook) # Desanexa o objeto da sessão
return db_notebook

def get_notebook_with_questions(db: Session, notebook_id: int, user_id:


int):
"""
Obtém um caderno pelo ID, incluindo suas questões e progresso do usuário.
"""
db_notebook = [Link]([Link]).filter(
[Link] == notebook_id,
[Link].user_id == user_id
).first()

if not db_notebook:
return None

# Converte de volta de string JSON para objetos Python


db_notebook.questoes_ids = [Link](db_notebook.questoes_ids)
db_notebook.filtros = [Link](db_notebook.filtros)

# Buscar as questões com base nos IDs


if db_notebook.questoes_ids:
questions = [Link]([Link]).filter(
[Link].in_(db_notebook.questoes_ids)
).all()
processed_questions = []
for q in questions:
q_dict = q.__dict__.copy() # Copia para evitar modificar o objeto
SQLAlchemy diretamente

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

db_notebook.questions_data = processed_questions # Adiciona a lista de


objetos Question ao caderno
else:
db_notebook.questions_data = []

# Carregar progresso SEPARADAMENTE


db_notebook.progress_data = get_notebook_progress(db, notebook_id,
user_id) # Chama a função existente

# Explicitamente desanexa o objeto notebook da sessão.


# Isso impede que ele seja implicitamente salvo (flushed) mais tarde
# se outros objetos na mesma sessão forem commitados.
[Link](db_notebook) # <--- LINHA CHAVE ADICIONADA AQUI

return db_notebook

def get_user_notebooks(db: Session, user_id: int) -> List[[Link]]:


"""
Lista todos os cadernos de um usuário específico, incluindo estatísticas
de progresso.
"""
notebooks = [Link]([Link]).filter([Link].user_id ==
user_id).all()
for notebook in notebooks:
# Converte de volta de string JSON para objetos Python
notebook.questoes_ids = [Link](notebook.questoes_ids)
[Link] = [Link]([Link])

# Adicionar contagem de questões para exibição no MyNotebooks


notebook.total_questoes = len(notebook.questoes_ids)

# Buscar progresso para cada caderno para obter acertos/respondidas


progress = [Link]([Link]).filter(
[Link].notebook_id == [Link],
[Link].user_id == user_id
).first()
if progress:
# Verifica se respostas é uma string JSON antes de tentar carregar
respostas_salvas = [Link]([Link]) if
isinstance([Link], str) else [Link]
[Link] = len(respostas_salvas)

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()

gabarito_map_multipla = {'A': 0, 'B': 1, 'C': 2, 'D': 3, 'E': 4}

for q_id, gabarito, tipo in questions_for_scoring:


user_answer = respostas_salvas.get(str(q_id))

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

def delete_notebook(db: Session, notebook_id: int):


"""
Deleta um caderno pelo ID.
Também deleta o progresso associado.
"""
db_notebook = [Link]([Link]).filter([Link] ==
notebook_id).first()
if db_notebook:
# Deletar progresso associado, se existir
db_progress =
[Link]([Link]).filter([Link].notebook_id
== notebook_id).first()
if db_progress:
[Link](db_progress)

[Link](db_notebook)
[Link]()
return True
return False

def update_notebook(db: Session, notebook_id: int, notebook_update:


[Link]):
"""
Atualiza um caderno existente pelo ID.
Permite apenas a atualização do nome.
"""
db_notebook = [Link]([Link]).filter([Link] ==
notebook_id).first()
if db_notebook:
if notebook_update.nome is not None:
db_notebook.nome = notebook_update.nome

# Não é necessário serializar aqui, pois NotebookUpdate só tem 'nome'


# Se outros campos fossem atualizados, eles precisariam ser
serializados.

[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

# --- Funções CRUD para Progresso de Caderno ---

def get_notebook_progress(db: Session, notebook_id: int, user_id: int):


"""
Obtém o progresso de um caderno para um usuário.
"""
progress = [Link]([Link]).filter(
[Link].notebook_id == notebook_id,
[Link].user_id == user_id
).first()
if progress:
# Verifica se respostas é uma string JSON antes de tentar carregar
[Link] = [Link]([Link]) if
isinstance([Link], str) else [Link]
return progress

def create_or_update_notebook_progress(db: Session, notebook_id: int,


user_id: int, progress_data: [Link]):
"""
Cria ou atualiza o progresso de um caderno para um usuário.
"""
db_progress = [Link]([Link]).filter(
[Link].notebook_id == notebook_id,
[Link].user_id == user_id
).first()

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

# --- Funções CRUD para Comentários ---

def create_comment(db: Session, question_id: int, user_id: int, content:


str):
"""
Cria um novo comentário para uma questão.
"""
db_comment = [Link](
question_id=question_id,
user_id=user_id,
content=content,
points=1 # Comentários começam com 1 ponto
)
[Link](db_comment)
[Link]()
[Link](db_comment)
return db_comment

def get_comments_for_question(db: Session, question_id: int, order_by: str =


"createdAt", order_direction: str = "desc"):
"""
Obtém todos os comentários para uma questão, com opções de ordenação.
"""
query = [Link]([Link]).filter([Link].question_id ==
question_id)

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

def update_comment(db: Session, comment_id: int, user_id: int, content:


str):
"""
Atualiza o conteúdo de um comentário.
"""
db_comment = [Link]([Link]).filter(
[Link] == comment_id,
[Link].user_id == user_id
).first()
if db_comment:
db_comment.content = content
[Link](db_comment)
[Link]()
[Link](db_comment)
db_comment.author # Carrega o autor para o schema
comment_dict = db_comment.__dict__.copy()
comment_dict['user'] = [Link](id=db_comment.[Link],
username=db_comment.[Link])
return [Link](**comment_dict)
return None

def delete_comment(db: Session, comment_id: int, user_id: int):


"""
Deleta um comentário.
"""
db_comment = [Link]([Link]).filter(
[Link] == comment_id,
[Link].user_id == user_id
).first()
if db_comment:
[Link](db_comment)
[Link]()
return True
return False

def vote_comment(db: Session, comment_id: int, user_id: int, vote_type:


str):
comment = [Link]([Link]).filter([Link] ==
comment_id).first()
if not comment:
raise HTTPException(status_code=404, detail="Comentário não encontrado")

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

# Garante que o autor do comentário venha junto na resposta


[Link] = [Link] if [Link] else
[Link]([Link]).filter_by(id=comment.user_id).first()
comment_dict = comment.__dict__.copy()
comment_dict['user'] = [Link](id=[Link],
username=[Link])
return [Link](**comment_dict)

def get_comments_with_vote_status(db: Session, question_id: int, user_id:


int):
comments = (
[Link]([Link])
.options(joinedload([Link])) # 🔑 carrega o autor junto
.filter([Link].question_id == question_id)
.order_by([Link].created_at.desc())
.all()
)

votes = (
[Link]([Link])
.filter([Link].user_id == user_id)
.filter([Link].comment_id.in_([[Link] for c in comments]))
.all()
)

vote_map = {v.comment_id: v.vote_type for v in votes}

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

# --- Funções CRUD para Teorias ---

def create_or_update_theory(db: Session, theory_data: [Link]):


"""
Cria ou atualiza uma teoria baseada na matéria e assunto.
"""
db_theory = [Link]([Link]).filter(
[Link] == theory_data.materia,
[Link] == theory_data.assunto
).first()

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

def get_all_theory_metadata(db: Session) -> List[[Link]]:


"""
Obtém metadados (matéria e assunto) de todas as teorias.
"""
theories = [Link]([Link],
[Link]).distinct().all()

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

def get_theory(db: Session, materia: str, assunto: str):


"""
Obtém o conteúdo de uma teoria específica.
"""
return [Link]([Link]).filter(
[Link] == materia,
[Link] == assunto
).first()

def delete_theory(db: Session, materia: str, assunto: str):


"""
Deleta uma teoria específica.
"""
db_theory = [Link]([Link]).filter(
[Link] == materia,
[Link] == assunto
).first()
if db_theory:
[Link](db_theory)
[Link]()
return True
return False

def get_note_by_user_and_question(db: Session, user_id: int, question_id:


int):
return [Link]([Link]).filter_by(user_id=user_id,
question_id=question_id).first()

def create_or_update_note(db: Session, user_id: int, note_data:


[Link]):
existing = get_note_by_user_and_question(db, user_id,
note_data.question_id)
if existing:
[Link] = note_data.content
[Link]()
[Link](existing)
return existing
new_note = [Link](user_id=user_id, **note_data.dict())
[Link](new_note)
[Link]()
[Link](new_note)
return new_note

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

from fastapi import FastAPI, Depends, HTTPException, status, UploadFile,


File, Form, Body
from [Link] import OAuth2PasswordRequestForm
from [Link] import Session
from typing import List, Dict, Any, Optional
from datetime import timedelta
from fastapi import Path
from typing import List, Dict, Any # certifique-se de ter essas importações
no topo
from [Link] import CORSMiddleware
from fastapi import Query
import logging
import json
import sys
import os

# Adiciona o diretório 'backend' ao [Link]


current_dir = [Link]([Link](__file__))
if current_dir not in [Link]:
[Link](0, current_dir)
import pdf_processor # Importe o módulo pdf_processor

# Importações relativas
from . import crud, models, schemas, auth
from .database import SessionLocal, engine, get_db

# Configura o logger para exibir mensagens INFO ou DEBUG


[Link](level=[Link])
logger = [Link](__name__)

# Cria as tabelas no banco de dados


[Link].create_all(bind=engine)

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=["*"],
)

# --- Endpoints de Autenticação ---

@[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.",
)

# Adiciona o 'role' e 'id' ao payload do token para fácil acesso no


frontend
access_token_expires = timedelta(minutes=auth.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = auth.create_access_token(
data={"sub": [Link], "role": [Link], "id": [Link]},
expires_delta=access_token_expires,
)
[Link](f"Token de acesso gerado para usuário: {[Link]} (Role:
{[Link]})")
return {"access_token": access_token, "token_type": "bearer"}

@[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")

db_user_username = crud.get_user_by_username(db, username=[Link])


if db_user_username:
[Link](f"Tentativa de registro com username já existente:
{[Link]}")
raise HTTPException(status_code=400, detail="Nome de usuário já existe")

hashed_password = auth.get_password_hash([Link])
[Link] = hashed_password

new_user = crud.create_user(db=db, user=user)


[Link](f"Novo usuário registrado: {new_user.username} (Role:
{new_user.role}, Ativo: {new_user.is_active})")
return new_user

# --- Endpoints de Gerenciamento de Usuários (Apenas para Admin) ---

@[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

# --- Endpoints de Questões ---

@[Link]("/api/questions/count-filtered/", response_model=Dict[str, Any])


async def count_filtered_questions(
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)
):
"""
Conta o número de questões com base nos filtros e retorna seus IDs.
Acessível por usuários e administradores.
"""
[Link](f"Usuário {current_user.username} contando questões com
filtros: Materia={materia}, Assunto={assunto}, Banca={banca}")
result = crud.count_questions(
db,
materia=materia, assunto=assunto, banca=banca,
orgao=orgao, cargo=cargo, ano=ano,
escolaridade=escolaridade, dificuldade=dificuldade, regiao=regiao
)
return result

@[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"}

# --- Endpoints de Estatísticas de Questões (Individual) ---

# NOVO ENDPOINT: GET para buscar estatísticas de uma questão


@[Link]("/api/questions/{question_id}/statistics",
response_model=[Link])
async def get_question_stats_route(
question_id: int,
db: Session = Depends(get_db),
current_user: [Link] = Depends(auth.get_current_user)
):
"""
Obtém as estatísticas de desempenho para uma questão específica.
"""
[Link](f"Usuário {current_user.username} buscando estatísticas para
questão ID: {question_id}")
stats = crud.get_question_statistics(db, question_id=question_id)
if not stats:
# Retorna estatísticas zeradas se não houver registro ainda
return [Link](question_id=question_id,
total_attempts=0, correct_attempts=0)
return stats

@[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()

for progress in user_progresses:


respostas = [Link]([Link]) if
isinstance([Link], str) else [Link]
user_answer = [Link](str(question_id))
if user_answer is not None:
user_attempts += 1
question = crud.get_question(db, question_id)
if question:
gabarito = [Link]()
tipo = [Link]
if tipo == "multipla":
g_map = {'A': 0, 'B': 1, 'C': 2, 'D': 3, 'E': 4}
if user_answer == g_map.get(gabarito):
user_corrects += 1
elif tipo == "certo_errado":
correto = 1 if [Link]() == "certo" else 0
if user_answer == correto:
user_corrects += 1

others_attempts = total.total_attempts - user_attempts


others_corrects = total.correct_attempts - user_corrects

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

# --- Endpoints de Cadernos ---

@[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")

if question_id is None or is_correct is None:


raise HTTPException(status_code=400, detail="Dados de resposta
incompletos (questaoId e acertou são obrigatórios).")

notebook = crud.get_notebook(db, notebook_id=notebook_id)


if not notebook or notebook.user_id != current_user.id:
raise HTTPException(status_code=403, detail="Acesso negado ou caderno
não encontrado.")

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"}

# --- Endpoints de Comentários ---

@[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])

# Retorna a resposta no formato esperado pelo frontend (incluindo o user)


return [Link](
id=db_comment.id,
question_id=db_comment.question_id,
user_id=db_comment.user_id,
content=db_comment.content,
points=db_comment.points,
created_at=db_comment.created_at,
updated_at=db_comment.updated_at,
user=[Link](
id=current_user.id,
username=current_user.username
)
)

@[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 type not in ["upvote", "downvote", "remove"]:


raise HTTPException(status_code=400, detail="Tipo de voto inválido. Use
'upvote', 'downvote' ou 'remove'.")
db_comment = crud.vote_comment(db, comment_id, current_user.id, type)

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}

# --- Endpoints de Teoria ---

@[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"}

# --- Endpoint de Upload de PDF (Placeholder) ---

@[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)

[Link](f"PDF '{[Link]}' processado com sucesso.


{num_questions_added} questões adicionadas.")
return {"message": f"PDF processado com sucesso! {num_questions_added}
questões adicionadas."}
except Exception as e:
[Link](f"Erro ao processar PDF '{[Link]}': {e}",
exc_info=True)
raise HTTPException(status_code=500, detail=f"Erro ao processar o PDF:
{str(e)}")

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"

id = Column(Integer, primary_key=True, index=True)


username = Column(String, unique=True, index=True, nullable=False)
email = Column(String, unique=True, index=True, nullable=False)
hashed_password = Column(String, nullable=False)
is_active = Column(Boolean, default=True)
role = Column(String, default="comum")

# ✅ 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"

id = Column(Integer, primary_key=True, index=True)


enunciado = Column(Text, nullable=False)
item_a = Column(Text)
item_b = Column(Text)
item_c = Column(Text)
item_d = Column(Text)
item_e = Column(Text)
materia = Column(String, index=True, nullable=False)
assunto = Column(String, index=True, nullable=False)
banca = Column(String, index=True, nullable=False)
orgao = Column(String, index=True)
cargo = Column(String, index=True)
ano = Column(Integer, index=True)
escolaridade = Column(String, index=True)
dificuldade = Column(String, index=True)
regiao = Column(String, index=True)
gabarito = Column(String, nullable=False) # 'A', 'B', 'C', 'D', 'E' ou
'Certo', 'Errado'
informacoes = Column(Text)
comentarioProfessor = Column(Text)
tipo = Column(String, nullable=False, default="multipla") # "multipla" ou
"certo_errado"

# 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"

id = Column(Integer, primary_key=True, index=True)


question_id = Column(Integer, ForeignKey("[Link]"), unique=True,
nullable=False)
total_attempts = Column(Integer, default=0)
correct_attempts = Column(Integer, default=0)

# 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"

id = Column(Integer, primary_key=True, index=True)


materia = Column(String, index=True, nullable=False)
assunto = Column(String, index=True, nullable=False)
content = Column(Text, nullable=False)

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"

id = Column(Integer, primary_key=True, index=True)


nome = Column(String, nullable=False)
user_id = Column(Integer, ForeignKey("[Link]"), nullable=False)

# Armazena os IDs das questões como uma string JSON


# Ex: "[1, 5, 10, 12]"
questoes_ids = Column(Text, nullable=False, default="[]")

# Armazena os filtros usados para criar o caderno como uma string JSON
# Ex: '{"materia": "Direito Penal", "banca": "Cebraspe"}'
filtros = Column(Text, nullable=False, default="{}")

paiId = Column(Integer, ForeignKey("[Link]"), nullable=True) # Para


pastas

# 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"

id = Column(Integer, primary_key=True, index=True, autoincrement=True)


notebook_id = Column(Integer, ForeignKey("[Link]"), unique=True,
nullable=False)
user_id = Column(Integer, ForeignKey("[Link]"), nullable=False) #
Adicionado user_id para consistência

# Armazena o índice da última questão resolvida


index = Column(Integer, default=0, nullable=False)

# Dicionário de question_id (str) para user_answer (int)


# Ex: '{"1": 0, "5": 2}' (questão 1 respondida com alt A, questão 5 com
alt C)
respostas = Column(Text, nullable=False, default="{}")

created_at = Column(DateTime(timezone=True), server_default=[Link]())


updated_at = Column(DateTime(timezone=True), onupdate=[Link]()) #
nullable=True é o padrão se não especificado
# Relacionamentos
notebook_rel = relationship("Notebook") # Modificado na versão anterior
(sem back_populates)
user_rel = relationship("User", back_populates="progresses")

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"

id = Column(Integer, primary_key=True, index=True)


question_id = Column(Integer, ForeignKey("[Link]"), nullable=False)
user_id = Column(Integer, ForeignKey("[Link]"), nullable=False)
content = Column(Text, nullable=False)
points = Column(Integer, default=0)
created_at = Column(DateTime(timezone=True), server_default=[Link]())
updated_at = Column(DateTime(timezone=True), onupdate=[Link]())

# 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})>"

# 🔽 Essa deve vir depois


class CommentVote(Base):
__tablename__ = "comment_votes"

id = Column(Integer, primary_key=True, index=True)


comment_id = Column(Integer, ForeignKey("[Link]"), nullable=False)
user_id = Column(Integer, ForeignKey("[Link]"), nullable=False)
vote_type = Column(String, nullable=False) # "upvote" ou "downvote"

__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"

id = Column(Integer, primary_key=True, index=True)


user_id = Column(Integer, ForeignKey("[Link]"), nullable=False)
question_id = Column(Integer, ForeignKey("[Link]"), nullable=False)
content = Column(Text, default="")
created_at = Column(DateTime(timezone=True), server_default=[Link]())
updated_at = Column(DateTime(timezone=True), onupdate=[Link]())

user = relationship("User")
question = relationship("Question")

__table_args__ = (UniqueConstraint("user_id", "question_id",


name="uq_user_question_note"),)

pdf_parser
colocar o código

schemas
from pydantic import BaseModel, EmailStr, Field
from typing import List, Optional, Dict, Any
from datetime import datetime

# --- Schemas de Autenticação e Usuário ---

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

# --- Schemas de Questão ---

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

# --- Schemas de Caderno ---

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

# --- Schemas de Comentário ---

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

# --- Schemas de Teoria ---

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

from [Link] import Session


import logging

logger = [Link](__name__)

def process_pdf_and_add_questions(db: Session, pdf_content: bytes) -> int:


"""
Função placeholder para processar o conteúdo de um PDF e adicionar
questões ao banco de dados.
"""
[Link]("Processando PDF (simulação)...")
# Simulação: número de questões adicionadas
num_questoes_adicionadas = 0

# TODO: implementar processamento real de PDF


return num_questoes_adicionadas

Você também pode gostar