Spaces:
Build error
Build error
| """ | |
| crud.py — Database CRUD operations. | |
| """ | |
| import json | |
| from sqlalchemy import func | |
| from sqlalchemy.orm import Session | |
| from . import models, schemas | |
| # --------------------------------------------------------------------------- | |
| # Client | |
| # --------------------------------------------------------------------------- | |
| def create_client(db: Session, data: schemas.ClientCreate) -> models.Client: | |
| """Create a new client profile.""" | |
| client = models.Client(**data.model_dump()) | |
| db.add(client) | |
| db.commit() | |
| db.refresh(client) | |
| return client | |
| def list_clients(db: Session) -> list[models.Client]: | |
| """Return all client profiles ordered by creation date (newest first).""" | |
| return db.query(models.Client).order_by(models.Client.created_at.desc()).all() | |
| def get_client(db: Session, client_id: int) -> models.Client | None: | |
| """Return a single client by ID, or None.""" | |
| return db.query(models.Client).filter(models.Client.id == client_id).first() | |
| # --------------------------------------------------------------------------- | |
| # Candidate | |
| # --------------------------------------------------------------------------- | |
| def create_candidate(db: Session, data: schemas.CandidateCreate) -> models.Candidate: | |
| """Create a new candidate linked to a client.""" | |
| candidate = models.Candidate(**data.model_dump()) | |
| db.add(candidate) | |
| db.commit() | |
| db.refresh(candidate) | |
| return candidate | |
| def list_candidates( | |
| db: Session, client_id: int | None = None | |
| ) -> list[models.Candidate]: | |
| """Return candidates, optionally filtered by client_id.""" | |
| query = db.query(models.Candidate) | |
| if client_id is not None: | |
| query = query.filter(models.Candidate.client_id == client_id) | |
| return query.order_by(models.Candidate.created_at.desc()).all() | |
| def get_candidate(db: Session, candidate_id: int) -> models.Candidate | None: | |
| """Return a single candidate by ID with eager-loaded score and feedback.""" | |
| return ( | |
| db.query(models.Candidate) | |
| .filter(models.Candidate.id == candidate_id) | |
| .first() | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Score (with mismatch detection) | |
| # --------------------------------------------------------------------------- | |
| _COMPETENCY_FIELDS = [ | |
| ("communication", "Communication"), | |
| ("adaptability", "Adaptability"), | |
| ("collaboration", "Collaboration"), | |
| ("problem_solving", "Problem Solving"), | |
| ("leadership", "Leadership"), | |
| ] | |
| def submit_scores( | |
| db: Session, candidate_id: int, data: schemas.ScoreCreate | |
| ) -> models.Score: | |
| """Score a candidate on the 5 BARS competencies and detect mismatches.""" | |
| candidate = get_candidate(db, candidate_id) | |
| if candidate is None: | |
| raise ValueError("Candidate not found") | |
| client = get_client(db, candidate.client_id) | |
| if client is None: | |
| raise ValueError("Client not found") | |
| # Build mismatch list | |
| mismatches: list[str] = [] | |
| passing = 0 | |
| total = len(_COMPETENCY_FIELDS) | |
| for field, label in _COMPETENCY_FIELDS: | |
| score_val = getattr(data, field) | |
| min_val = getattr(client, f"min_{field}") | |
| if score_val >= min_val: | |
| passing += 1 | |
| else: | |
| mismatches.append( | |
| f"{label}: scored {score_val}, minimum required {min_val}" | |
| ) | |
| overall_match = passing / total if total > 0 else 0.0 | |
| # Check if score already exists for this candidate | |
| existing = ( | |
| db.query(models.Score) | |
| .filter(models.Score.candidate_id == candidate_id) | |
| .first() | |
| ) | |
| if existing: | |
| # Update existing score | |
| for field, _ in _COMPETENCY_FIELDS: | |
| setattr(existing, field, getattr(data, field)) | |
| existing.overall_match = overall_match | |
| existing.mismatches = json.dumps(mismatches) | |
| db.commit() | |
| db.refresh(existing) | |
| return existing | |
| # Create new score | |
| score = models.Score( | |
| candidate_id=candidate_id, | |
| **data.model_dump(), | |
| overall_match=overall_match, | |
| mismatches=json.dumps(mismatches), | |
| ) | |
| db.add(score) | |
| db.commit() | |
| db.refresh(score) | |
| return score | |
| # --------------------------------------------------------------------------- | |
| # Feedback | |
| # --------------------------------------------------------------------------- | |
| def submit_feedback( | |
| db: Session, candidate_id: int, data: schemas.FeedbackCreate | |
| ) -> models.Feedback: | |
| """Record post-interview feedback for a candidate.""" | |
| # Check if feedback already exists | |
| existing = ( | |
| db.query(models.Feedback) | |
| .filter(models.Feedback.candidate_id == candidate_id) | |
| .first() | |
| ) | |
| if existing: | |
| existing.outcome = data.outcome | |
| existing.primary_reason = data.primary_reason | |
| existing.client_notes = data.client_notes | |
| db.commit() | |
| db.refresh(existing) | |
| return existing | |
| feedback = models.Feedback(candidate_id=candidate_id, **data.model_dump()) | |
| db.add(feedback) | |
| db.commit() | |
| db.refresh(feedback) | |
| return feedback | |
| # --------------------------------------------------------------------------- | |
| # Stats | |
| # --------------------------------------------------------------------------- | |
| def get_stats(db: Session) -> dict: | |
| """Compute dashboard statistics.""" | |
| total_candidates = db.query(func.count(models.Candidate.id)).scalar() or 0 | |
| total_with_feedback = ( | |
| db.query(func.count(models.Feedback.id)).scalar() or 0 | |
| ) | |
| accepted_count = ( | |
| db.query(func.count(models.Feedback.id)) | |
| .filter(models.Feedback.outcome == "accepted") | |
| .scalar() | |
| or 0 | |
| ) | |
| rejected_count = ( | |
| db.query(func.count(models.Feedback.id)) | |
| .filter(models.Feedback.outcome == "rejected") | |
| .scalar() | |
| or 0 | |
| ) | |
| acceptance_rate = ( | |
| (accepted_count / total_with_feedback * 100) | |
| if total_with_feedback > 0 | |
| else 0.0 | |
| ) | |
| feedback_compliance_rate = ( | |
| (total_with_feedback / total_candidates * 100) | |
| if total_candidates > 0 | |
| else 0.0 | |
| ) | |
| return { | |
| "total_candidates": total_candidates, | |
| "total_with_feedback": total_with_feedback, | |
| "accepted_count": accepted_count, | |
| "rejected_count": rejected_count, | |
| "acceptance_rate": round(acceptance_rate, 1), | |
| "feedback_compliance_rate": round(feedback_compliance_rate, 1), | |
| } | |