departements
This commit is contained in:
parent
6b53a419c9
commit
4c180fe1f8
19 changed files with 21999 additions and 431 deletions
|
@ -107,9 +107,14 @@ async def analyze_job_offer_and_cv_route(
|
|||
|
||||
if not cv_document:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="CV non trouvé ou non accessible par cet utilisateur.")
|
||||
|
||||
try:
|
||||
# Utilise la nouvelle fonction d'extraction de texte
|
||||
logger.info(f"Tentative d'extraction du texte du CV à partir de : {cv_document.filepath}") # AJOUTEZ CETTE
|
||||
|
||||
# --- AJOUTEZ CES LIGNES DE DEBUG ---
|
||||
logger.info(f"Texte extrait (début) du CV: '{cv_text_to_analyze[:100]}...'") # Affiche les 100 premiers caractères
|
||||
logger.info(f"Longueur du texte extrait du CV (avant strip): {len(cv_text_to_analyze)}")
|
||||
logger.info(f"Longueur du texte extrait du CV (après strip): {len(cv_text_to_analyze.strip())}")
|
||||
# --- FIN DES LIGNES DE DEBUG ---
|
||||
cv_text_to_analyze = extract_text_from_file(cv_document.filepath)
|
||||
if not cv_text_to_analyze.strip(): # Vérifier après extraction si le contenu est vide
|
||||
raise ValueError("Le fichier CV est vide ou l'extraction de texte a échoué.")
|
||||
|
|
|
@ -1,17 +1,23 @@
|
|||
# backend/routers/document.py
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
|
||||
from fastapi.responses import FileResponse
|
||||
from sqlalchemy.orm import Session
|
||||
import os
|
||||
import uuid # Pour générer des noms de fichiers uniques
|
||||
import uuid # For generating unique filenames
|
||||
import logging
|
||||
from typing import List # Required for list type hint in get_user_documents
|
||||
|
||||
from core.database import get_db
|
||||
from core.security import create_access_token # Non utilisé directement ici mais potentiellement dans d'autres routers
|
||||
from core.config import settings # Pour accéder au chemin d'upload
|
||||
# Removed unused 'create_access_token'
|
||||
from core.security import get_current_user # Ensure this is the correct import for your get_current_user dependency
|
||||
from core.config import settings # To access upload directory
|
||||
from crud import document as crud_document
|
||||
from crud import user as crud_user # Pour récupérer l'utilisateur courant
|
||||
# Removed unused 'crud_user' as it's not directly used in this router
|
||||
from schemas import document as schemas_document
|
||||
from schemas import user as schemas_user # Pour le modèle UserInDBBase ou UserResponse
|
||||
from dependencies import get_current_user # Pour la protection des routes
|
||||
from schemas import user as schemas_user # For UserResponse schema
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/documents",
|
||||
|
@ -19,7 +25,7 @@ router = APIRouter(
|
|||
responses={404: {"description": "Not found"}},
|
||||
)
|
||||
|
||||
@router.post("/upload-cv", response_model=schemas_document.DocumentResponse, status_code=status.HTTP_201_CREATED)
|
||||
@router.post("/upload-cv", response_model=schemas_document.DocumentResponse, status_code=status.HTTP_201_CREATED, summary="Uploader un CV")
|
||||
async def upload_cv(
|
||||
file: UploadFile = File(...),
|
||||
db: Session = Depends(get_db),
|
||||
|
@ -29,39 +35,53 @@ async def upload_cv(
|
|||
Permet à un utilisateur authentifié d'uploader un CV.
|
||||
Le fichier est stocké sur le serveur et ses métadonnées sont enregistrées en base de données.
|
||||
"""
|
||||
if not file.filename.lower().endswith(('.pdf', '.doc', '.docx')):
|
||||
logger.info(f"Tentative d'upload de CV par l'utilisateur {current_user.id} - Nom du fichier: {file.filename}")
|
||||
|
||||
if not file.filename:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Le nom du fichier est manquant.")
|
||||
|
||||
allowed_extensions = ('.pdf', '.doc', '.docx')
|
||||
# Use os.path.splitext to safely get the extension
|
||||
file_extension = os.path.splitext(file.filename)[1].lower()
|
||||
if file_extension not in allowed_extensions:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Seuls les fichiers PDF, DOC, DOCX sont autorisés."
|
||||
detail=f"Seuls les fichiers {', '.join(allowed_extensions).upper()} sont autorisés."
|
||||
)
|
||||
|
||||
# Créer un nom de fichier unique pour éviter les collisions et les problèmes de sécurité
|
||||
unique_filename = f"{uuid.uuid4()}_{file.filename}"
|
||||
file_path = os.path.join(settings.UPLOADS_DIR, unique_filename)
|
||||
upload_dir = settings.UPLOADS_DIR # Utilisez le chemin absolu configuré dans settings
|
||||
os.makedirs(upload_dir, exist_ok=True)
|
||||
|
||||
# S'assurer que le répertoire d'uploads existe
|
||||
os.makedirs(settings.UPLOADS_DIR, exist_ok=True)
|
||||
# Generate a unique filename using UUID to prevent collisions and potential path traversal issues
|
||||
unique_filename = f"{uuid.uuid4()}{file_extension}"
|
||||
file_path = os.path.join(upload_dir, unique_filename)
|
||||
|
||||
try:
|
||||
# Write the file in chunks for efficiency with large files
|
||||
with open(file_path, "wb") as buffer:
|
||||
# Écrit le fichier par morceaux pour les gros fichiers
|
||||
while content := await file.read(1024 * 1024): # Lire par blocs de 1MB
|
||||
while content := await file.read(1024 * 1024): # Read in 1MB chunks
|
||||
buffer.write(content)
|
||||
logger.info(f"Fichier '{file.filename}' enregistré sous '{file_path}' pour l'utilisateur {current_user.id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'enregistrement du fichier {file.filename}: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Erreur lors de l'enregistrement du fichier: {e}"
|
||||
)
|
||||
finally:
|
||||
# Ensure the UploadFile is closed even if an error occurs
|
||||
await file.close()
|
||||
|
||||
# Enregistrer les métadonnées du document dans la base de données
|
||||
# Save document metadata in the database
|
||||
# The DocumentCreate schema might not need 'filename' as a field if you pass it directly to crud
|
||||
# Assuming DocumentCreate schema only takes filename and crud.create_document handles filepath
|
||||
document_data = schemas_document.DocumentCreate(filename=file.filename)
|
||||
db_document = crud_document.create_document(db, document_data, file_path, current_user.id)
|
||||
logger.info(f"Document ID {db_document.id} créé en base de données pour l'utilisateur {current_user.id}")
|
||||
|
||||
return db_document
|
||||
|
||||
@router.get("/", response_model=list[schemas_document.DocumentResponse])
|
||||
@router.get("/", response_model=List[schemas_document.DocumentResponse], summary="Lister les documents de l'utilisateur")
|
||||
def get_user_documents(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: schemas_user.UserResponse = Depends(get_current_user)
|
||||
|
@ -69,26 +89,34 @@ def get_user_documents(
|
|||
"""
|
||||
Récupère tous les documents uploadés par l'utilisateur authentifié.
|
||||
"""
|
||||
logger.info(f"Tentative de listage des documents pour l'utilisateur {current_user.id}")
|
||||
documents = crud_document.get_documents_by_owner(db, current_user.id)
|
||||
return documents
|
||||
|
||||
@router.get("/{document_id}", response_model=schemas_document.DocumentResponse)
|
||||
@router.get("/{document_id}", response_model=schemas_document.DocumentResponse, summary="Récupérer un document par ID")
|
||||
def get_document_details(
|
||||
document_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: schemas_user.UserResponse = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Récupère les détails d'un document spécifique de l'utilisateur authentifié.
|
||||
Récupère les détails d'un document spécifique appartenant à l'utilisateur courant.
|
||||
"""
|
||||
document = crud_document.get_document_by_id(db, document_id)
|
||||
logger.info(f"Tentative de récupération du document {document_id} pour l'utilisateur {current_user.id}")
|
||||
|
||||
# Appel à la fonction CRUD qui filtre déjà par owner_id
|
||||
document = crud_document.get_document_by_id(db, document_id, current_user.id)
|
||||
|
||||
if not document:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document non trouvé.")
|
||||
if document.owner_id != current_user.id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Vous n'avez pas accès à ce document.")
|
||||
# Si le document n'est pas trouvé (soit il n'existe pas, soit il n'appartient pas à cet utilisateur)
|
||||
logger.warning(f"Document {document_id} non trouvé ou non autorisé pour l'utilisateur {current_user.id}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Document non trouvé ou vous n'avez pas l'autorisation d'y accéder."
|
||||
)
|
||||
return document
|
||||
|
||||
@router.delete("/{document_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete("/{document_id}", status_code=status.HTTP_204_NO_CONTENT, summary="Supprimer un document par ID")
|
||||
async def delete_document(
|
||||
document_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
|
@ -98,22 +126,62 @@ async def delete_document(
|
|||
Supprime un document spécifique de l'utilisateur authentifié,
|
||||
à la fois de la base de données et du système de fichiers.
|
||||
"""
|
||||
db_document = crud_document.get_document_by_id(db, document_id)
|
||||
if not db_document:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document non trouvé.")
|
||||
if db_document.owner_id != current_user.id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Vous n'avez pas la permission de supprimer ce document.")
|
||||
logger.info(f"Tentative de suppression du document {document_id} pour l'utilisateur {current_user.id}")
|
||||
|
||||
# Supprimer le fichier du système de fichiers
|
||||
if os.path.exists(db_document.filepath):
|
||||
# Appel à la fonction CRUD qui filtre déjà par owner_id
|
||||
db_document = crud_document.get_document_by_id(db, document_id, current_user.id)
|
||||
|
||||
if not db_document:
|
||||
# Si le document n'est pas trouvé (soit il n'existe pas, soit il n'appartient pas à cet utilisateur)
|
||||
logger.warning(f"Document {document_id} non trouvé ou non autorisé pour la suppression par l'utilisateur {current_user.id}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Document non trouvé ou vous n'avez pas la permission de le supprimer."
|
||||
)
|
||||
|
||||
# Supprimer le fichier du système de fichiers s'il existe et si un chemin est défini
|
||||
if db_document.filepath and os.path.exists(db_document.filepath):
|
||||
try:
|
||||
os.remove(db_document.filepath)
|
||||
logger.info(f"Fichier physique '{db_document.filepath}' supprimé pour le document {document_id}.")
|
||||
except OSError as e:
|
||||
logger.error(f"Erreur lors de la suppression du fichier physique '{db_document.filepath}' pour le document {document_id}: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Erreur lors de la suppression du fichier sur le serveur: {e}"
|
||||
)
|
||||
else:
|
||||
logger.warning(f"Le document {document_id} n'a pas de chemin de fichier ou le fichier n'existe pas: {db_document.filepath}")
|
||||
|
||||
# Supprimer l'entrée de la base de données
|
||||
crud_document.delete_document(db, document_id)
|
||||
return {"message": "Document supprimé avec succès."}
|
||||
success = crud_document.delete_document(db, document_id)
|
||||
if not success:
|
||||
logger.error(f"Échec de la suppression de l'entrée du document {document_id} de la base de données.")
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Échec de la suppression du document de la base de données.")
|
||||
|
||||
logger.info(f"Document {document_id} et son fichier physique supprimés avec succès pour l'utilisateur {current_user.id}.")
|
||||
return {} # 204 No Content typically returns an empty body
|
||||
|
||||
# Optional: Add a route to download the actual file if needed
|
||||
@router.get("/{document_id}/download", summary="Télécharger un document")
|
||||
async def download_document(
|
||||
document_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: schemas_user.UserResponse = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Permet à l'utilisateur authentifié de télécharger un de ses documents.
|
||||
"""
|
||||
logger.info(f"Tentative de téléchargement du document {document_id} par l'utilisateur {current_user.id}")
|
||||
|
||||
db_document = crud_document.get_document_by_id(db, document_id, current_user.id)
|
||||
if not db_document:
|
||||
logger.warning(f"Document {document_id} non trouvé ou non autorisé pour le téléchargement par l'utilisateur {current_user.id}")
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document non trouvé ou non autorisé.")
|
||||
|
||||
if not os.path.exists(db_document.filepath):
|
||||
logger.error(f"Fichier physique non trouvé pour le document {document_id} à l'emplacement: {db_document.filepath}")
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Fichier physique non trouvé sur le serveur.")
|
||||
|
||||
# Return the file as a FastAPI FileResponse
|
||||
return FileResponse(path=db_document.filepath, filename=db_document.filename, media_type="application/octet-stream")
|
|
@ -1,73 +1,62 @@
|
|||
# backend/routers/france_travail_offers.py
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
|
||||
from services.france_travail_offer_service import france_travail_offer_service
|
||||
from core.security import get_current_user
|
||||
from models.user import User
|
||||
# Assuming these imports are still needed for your project context,
|
||||
# even if not directly used in the current problem scope.
|
||||
# from core.security import get_current_user
|
||||
# from models.user import User
|
||||
from schemas.france_travail import FranceTravailSearchResponse, OffreDetail, Offre
|
||||
|
||||
import logging
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@router.get("/search", response_model=FranceTravailSearchResponse)
|
||||
async def search_france_travail_offers(
|
||||
motsCles: Optional[str] = Query(None, description="Mots-clés de recherche (ex: 'développeur full stack')"),
|
||||
commune_nom_ou_code: Optional[str] = Query(None, alias="commune", description="Nom, code postal ou code INSEE de la commune"),
|
||||
distance: Optional[int] = Query(10, description="Distance maximale en km autour de la commune"),
|
||||
commune_input: Optional[str] = Query(None, alias="commune", description="Nom de la commune (ex: 'Paris', 'Marseille'). Si spécifié, le code départemental sera automatiquement dérivé."),
|
||||
distance: Optional[int] = Query(10, description="Distance maximale en km autour de la commune ou du code postal. Applicable avec 'commune' ou 'codePostal', 'latitude'/'longitude'."),
|
||||
codePostal: Optional[str] = Query(None, description="Code postal spécifique (ex: '75001')"),
|
||||
latitude: Optional[float] = Query(None, description="Latitude du point de recherche (ex: 48.8566)"),
|
||||
longitude: Optional[float] = Query(None, description="Longitude du point de recherche (ex: 2.3522)"),
|
||||
# codeDepartement: Optional[str] = Query(None, description="Code départemental sur 2 chiffres (ex: '75' pour Paris). Prioritaire sur les autres paramètres de localisation."), # Ce paramètre est maintenant géré en interne par le service
|
||||
page: int = Query(0, description="Numéro de la page de résultats (commence à 0)"),
|
||||
limit: int = Query(15, description="Nombre d'offres par page (max 100 pour l'API France Travail)"), # Max 100 est une limite courante pour une seule requête à l'API FT
|
||||
contrat: Optional[str] = Query(None, description="Type de contrat (ex: 'CDI', 'CDD', 'MIS')"),
|
||||
experience: Optional[str] = Query(None, description="Niveau d'expérience (ex: '1' pour débutant, '2' pour 1-3 ans, '3' pour >3 ans)"),
|
||||
current_user: User = Depends(get_current_user)
|
||||
limit: int = Query(15, description="Nombre d'offres par page (max 100 pour l'API France Travail)"),
|
||||
contrat: Optional[str] = Query(None, description="Type de contrat (ex: 'CDI', 'CDD', 'MIS'). Plusieurs séparés par des virgules."),
|
||||
experience: Optional[str] = Query(None, description="Niveau d'expérience (ex: 'E' pour expérimenté, 'D' pour débutant). Plusieurs séparés par des virgules.")
|
||||
# current_user: User = Depends(get_current_user) # Décommentez si l'authentification est nécessaire
|
||||
):
|
||||
"""
|
||||
Recherche des offres d'emploi via l'API France Travail.
|
||||
Convertit le nom de ville en code INSEE si nécessaire et gère la pagination.
|
||||
Nécessite une authentification.
|
||||
La localisation peut être spécifiée par commune (le département sera dérivé), code postal, ou latitude/longitude.
|
||||
"""
|
||||
if limit > 100: # La limite de l'API France Travail pour 'range' est souvent 150 ou 100 items par requête.
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="La limite de résultats par page ne peut pas dépasser 100 pour une seule requête API."
|
||||
)
|
||||
logger.info(f"Requête de recherche d'offres reçue: motsCles='{motsCles}', commune_input='{commune_input}', codePostal='{codePostal}', latitude='{latitude}', longitude='{longitude}', distance={distance}, page={page}, limit={limit}")
|
||||
|
||||
commune_param_for_api = None
|
||||
range_start = page * limit
|
||||
range_end = range_start + limit - 1
|
||||
logger.info(f"Paramètre 'range' calculé pour l'API France Travail: {range_start}-{range_end}")
|
||||
|
||||
if commune_nom_ou_code:
|
||||
if commune_nom_ou_code.isdigit() and len(commune_nom_ou_code) == 5:
|
||||
commune_param_for_api = commune_nom_ou_code
|
||||
logger.info(f"Recherche par code postal: {commune_nom_ou_code}")
|
||||
else:
|
||||
logger.info(f"Tentative de récupération du code INSEE pour la ville: {commune_nom_ou_code}")
|
||||
insee_code = await france_travail_offer_service.get_insee_code_for_commune(commune_nom_ou_code)
|
||||
if not insee_code:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Code INSEE non trouvé pour la ville '{commune_nom_ou_code}'. Veuillez vérifier l'orthographe ou utiliser un code postal."
|
||||
)
|
||||
commune_param_for_api = insee_code
|
||||
logger.info(f"Code INSEE '{insee_code}' trouvé pour '{commune_nom_ou_code}'.")
|
||||
# Convertir les chaînes de contrats et expériences en listes
|
||||
contrats_list = contrat.split(',') if contrat else None
|
||||
experiences_list = experience.split(',') if experience else None
|
||||
|
||||
# Les paramètres de localisation sont passés directement au service,
|
||||
# qui gérera la dérivation du département et la priorité.
|
||||
|
||||
if (commune_param_for_api is not None) and (distance is None):
|
||||
distance = 10
|
||||
|
||||
# Calcul du paramètre 'range' pour l'API France Travail
|
||||
start_index = page * limit
|
||||
end_index = start_index + limit - 1
|
||||
api_range_param = f"{start_index}-{end_index}"
|
||||
logger.info(f"Paramètre 'range' calculé pour l'API France Travail: {api_range_param}")
|
||||
|
||||
try:
|
||||
response = await france_travail_offer_service.search_offers(
|
||||
motsCles=motsCles,
|
||||
commune=commune_param_for_api,
|
||||
commune=commune_input, # Passe le nom de la commune directement
|
||||
codePostal=codePostal,
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
distance=distance,
|
||||
range=api_range_param, # On passe le 'range' calculé
|
||||
typeContrat=contrat,
|
||||
# experience=experience # Vérifiez si ce paramètre est géré par l'API France Travail ou doit être mappé
|
||||
# codeDepartement n'est plus passé ici, il est dérivé dans le service
|
||||
range_start=range_start,
|
||||
range_end=range_end,
|
||||
typeContrat=contrats_list,
|
||||
experience=experiences_list
|
||||
)
|
||||
return response
|
||||
except RuntimeError as e:
|
||||
|
@ -80,7 +69,7 @@ async def search_france_travail_offers(
|
|||
@router.get("/{offer_id}", response_model=OffreDetail)
|
||||
async def get_france_travail_offer_details(
|
||||
offer_id: str,
|
||||
current_user: User = Depends(get_current_user)
|
||||
# current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Récupère les détails d'une offre d'emploi spécifique de l'API France Travail par son ID.
|
||||
|
@ -93,5 +82,5 @@ async def get_france_travail_offer_details(
|
|||
logger.error(f"Erreur lors de la récupération des détails de l'offre {offer_id} de France Travail: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Impossible de récupérer les détails de l'offre: {e}"
|
||||
)
|
||||
detail=f"Impossible de récupérer les détails de l'offre {offer_id}: {e}"
|
||||
)
|
Loading…
Add table
Add a link
Reference in a new issue