"""Pure helpers for the hiring forms domain — no FastAPI, no DB. FORM_DEFINITIONS is the single authority for section/criterion/field keys AND their on-screen labels, which reproduce the paper annexures verbatim (Annexure A Employee Requisition Form, Annexure E Interview Evaluation Form). The frontend renders labels from /forms/definitions, and criterion labels are denormalized into every saved row so historical records survive future renames. """ FORM_TYPES = ("requisition", "interview_analysis", "cultural_fit") RATING_MIN = 1 RATING_MAX = 4 RATING_LABELS = { 1: "Below Average (1)", 2: "Average (2)", 3: "Good (3)", 4: "Excellent (4)", } RATING_SCALE_NOTE = ( "Rating Scale: 1 = Below Average | 2 = Average | 3 = Good | 4 = Excellent. " "Tick the box that applies for each criterion." ) RECOMMENDATIONS = ( "selected", "hold", "next_round", "not_selected", "other_position", "offer_placement", ) RECOMMENDATION_LABELS = { "selected": "Selected", "hold": "Hold for now", "next_round": "Shortlist for next round", "not_selected": "Not selected", "other_position": "Consider for other position", "offer_placement": "Offer Placement", } # INTERVIEW onward; APPROVED is the legacy spelling the UI maps to Hired. FORM_READY_STATUSES = ("INTERVIEW", "OFFER", "HIRED", "APPROVED") EMPLOYMENT_TYPES = ("permanent", "temporary", "contract", "internee") EMPLOYMENT_TYPE_LABELS = { "permanent": "Permanent", "temporary": "Temporary", "contract": "Contract", "internee": "Internee", } _EVALUATION_HEADER_FIELDS = [ {"key": "interviewer_name", "label": "Interviewer Name", "kind": "text"}, {"key": "department", "label": "Department/Division", "kind": "text"}, {"key": "position_title", "label": "Position Interviewed For", "kind": "text"}, ] _EVALUATION_FOOTER_FIELDS = [ {"key": "strengths", "label": "Key Strengths", "kind": "textarea"}, {"key": "concerns", "label": "Main Concerns or Gaps", "kind": "textarea"}, { "key": "overall_observation", "label": "Overall Observation of the Candidate", "kind": "textarea", }, ] FORM_DEFINITIONS = { "interview_analysis": { "title": "Interview Analysis", "source": "Annexure E - Interview Evaluation Form", "scale_note": RATING_SCALE_NOTE, "sections": [ { "key": "technical", "title": "TECHNICAL COMPETENCY ASSESSMENT", "average_label": "TECHNICAL SECTION AVERAGE", "criteria": [ {"key": "core_job_knowledge", "label": "Core Job Knowledge & Domain Expertise"}, {"key": "relevant_experience", "label": "Depth of Relevant Experience"}, {"key": "problem_solving", "label": "Problem Solving & Analytical Reasoning"}, {"key": "tools_proficiency", "label": "Technical Tools & Systems Proficiency"}, {"key": "quality_of_work", "label": "Quality of Work & Attention to Detail"}, ], }, { "key": "behavioral", "title": "BEHAVIORAL COMPETENCY ASSESSMENT", "average_label": "BEHAVIORAL SECTION AVERAGE", "criteria": [ {"key": "communication", "label": "Communication & Clarity of Expression"}, {"key": "active_listening", "label": "Active Listening & Comprehension"}, {"key": "ownership", "label": "Ownership & Accountability"}, {"key": "resilience", "label": "Resilience Under Pressure"}, {"key": "learning_agility", "label": "Learning Agility & Coachability"}, ], }, ], "fields": ( _EVALUATION_HEADER_FIELDS + [ {"key": "summary", "label": "BRIEF SUMMARY OF THE CANDIDATE", "kind": "textarea"}, {"key": "technical_note", "label": "Technical Competency — Notes", "kind": "text"}, {"key": "behavioral_note", "label": "Behavioral Competency — Notes", "kind": "text"}, ] + _EVALUATION_FOOTER_FIELDS ), "has_recommendation": True, }, "cultural_fit": { "title": "Cultural Fit", "source": "Annexure E - Interview Evaluation Form", "scale_note": RATING_SCALE_NOTE, "sections": [ { "key": "cultural", "title": "CULTURAL FIT", "average_label": "CULTURAL FIT SECTION", "criteria": [ {"key": "company_values", "label": "Alignment with Company Values"}, {"key": "professionalism", "label": "Professionalism & Integrity"}, {"key": "collaboration", "label": "Collaboration & Team Orientation"}, {"key": "adaptability", "label": "Adaptability to Change"}, {"key": "work_ethic", "label": "Work Ethic & Reliability"}, ], }, ], "fields": ( _EVALUATION_HEADER_FIELDS + [{"key": "cultural_note", "label": "Cultural Fit — Notes", "kind": "text"}] + _EVALUATION_FOOTER_FIELDS ), "has_recommendation": True, }, "requisition": { "title": "Employee Requisition", "source": "Annexure A - Employee Requisition Form", "header_note": "To: Human Resource Department", "sections": [], "fields": [ {"key": "department", "label": "From: (Dept.)", "kind": "text"}, {"key": "job_title", "label": "Job Title", "kind": "text"}, {"key": "date_needed", "label": "Date Needed", "kind": "date"}, { "key": "employment_type", "label": "Permanent / Temporary / Contract / Internee", "kind": "select", "options": list(EMPLOYMENT_TYPES), }, {"key": "period_from", "label": "If not permanent, specify the period — From", "kind": "date"}, {"key": "period_to", "label": "If not permanent, specify the period — To", "kind": "date"}, { "key": "jd_available", "label": ( "JD Available (JD is mandatory, TA team will not proceed with " "sourcing until JD is provided)" ), "kind": "bool", }, {"key": "is_replacement", "label": "IF A REPLACEMENT, COMPLETE THE FOLLOWING", "kind": "bool"}, {"key": "replacement_employee", "label": "Employee to be replaced", "kind": "text"}, {"key": "replacement_grade", "label": "Grade", "kind": "text"}, {"key": "replacement_job_title", "label": "Job Title (replaced employee)", "kind": "text"}, {"key": "replacement_date_separated", "label": "Date Separated", "kind": "date"}, { "key": "headcount_justification", "label": "IN CASE OF NEW/ADDITIONAL HEADCOUNT PLEASE PROVIDE JUSTIFICATION", "kind": "textarea", }, {"key": "proposed_budget", "label": "PROPOSE BUDGET", "kind": "text"}, {"key": "recommended_grade", "label": "RECOMMENDED GRADE", "kind": "text"}, {"key": "internal_recommendation", "label": "INCASE OF INTERNAL RECOMMENDATE", "kind": "bool"}, {"key": "recommended_employee_name", "label": "EMPLOYEE NAME", "kind": "text"}, {"key": "recommended_employee_department", "label": "EMPLOYEE DEPARTMENT", "kind": "text"}, {"key": "initiated_by", "label": "Initiated By — Name", "kind": "text"}, {"key": "initiated_date", "label": "Initiated By — Date", "kind": "date"}, {"key": "recommended_by", "label": "Recommended By — Name (Director)", "kind": "text"}, {"key": "recommended_date", "label": "Recommended By — Date", "kind": "date"}, {"key": "approved_by", "label": "Approved By — Name (Director HR)", "kind": "text"}, {"key": "approved_date", "label": "Approved By — Date", "kind": "date"}, {"key": "vp_approved_by", "label": "Approved By — Name (VP/SVP)", "kind": "text"}, {"key": "vp_approved_date", "label": "Approved By — Date (VP/SVP)", "kind": "date"}, ], "field_enums": {"employment_type": EMPLOYMENT_TYPES}, "has_recommendation": False, }, } def definitions_payload() -> dict: """The response body for GET /forms/definitions.""" return { "form_types": list(FORM_TYPES), "forms": FORM_DEFINITIONS, "rating_labels": {str(k): v for k, v in RATING_LABELS.items()}, "recommendations": list(RECOMMENDATIONS), "recommendation_labels": dict(RECOMMENDATION_LABELS), "employment_types": list(EMPLOYMENT_TYPES), "employment_type_labels": dict(EMPLOYMENT_TYPE_LABELS), "form_ready_statuses": list(FORM_READY_STATUSES), } def _coerce_rating(value): if value in (None, ""): return None try: number = float(value) except (TypeError, ValueError): raise ValueError(f"rating must be a number, got {value!r}") if number != int(number): raise ValueError(f"rating must be a whole number, got {value!r}") rating = int(number) if rating < RATING_MIN or rating > RATING_MAX: raise ValueError(f"rating must be between {RATING_MIN} and {RATING_MAX}, got {rating}") return rating def _mean(values, digits=2): values = [v for v in values if v is not None] if not values: return None return round(sum(values) / len(values), digits) def normalize_sections(form_type: str, sections): """Validate submitted rated sections against the form definition and recompute all derived numbers. Returns (normalized_sections, overall_score). Every definition section is emitted in definition order with denormalized labels; submitted per-criterion ratings are merged in; client-sent averages are discarded and recomputed (mean of the non-null ratings, 2 dp). The overall score is the mean of the section averages. Raises ValueError on unknown section/criterion keys or out-of-range ratings (422 material). """ definition = FORM_DEFINITIONS.get(form_type) if definition is None: raise ValueError(f"unknown form_type {form_type!r}") if not definition["sections"]: return None, None if sections is None: sections = [] if not isinstance(sections, list): raise ValueError("sections must be a list") known_sections = {s["key"]: s for s in definition["sections"]} submitted = {} for entry in sections: if not isinstance(entry, dict): raise ValueError("each section must be an object") key = entry.get("key") if key not in known_sections: raise ValueError(f"unknown section {key!r} for {form_type}") criteria = entry.get("criteria") or [] if not isinstance(criteria, list): raise ValueError("section criteria must be a list") known_criteria = {c["key"] for c in known_sections[key]["criteria"]} ratings = {} for criterion in criteria: if not isinstance(criterion, dict): raise ValueError("each criterion must be an object") ckey = criterion.get("key") if ckey not in known_criteria: raise ValueError(f"unknown criterion {ckey!r} in section {key!r}") ratings[ckey] = _coerce_rating(criterion.get("rating")) submitted[key] = ratings normalized = [] section_averages = [] for section_def in definition["sections"]: ratings = submitted.get(section_def["key"], {}) criteria = [ { "key": c["key"], "label": c["label"], "rating": ratings.get(c["key"]), } for c in section_def["criteria"] ] average = _mean([c["rating"] for c in criteria]) if average is not None: section_averages.append(average) normalized.append( { "key": section_def["key"], "title": section_def["title"], "criteria": criteria, "average": average, } ) return normalized, _mean(section_averages) def normalize_fields(form_type: str, fields): """Keep only the definition's field keys, validate enums, coerce booleans.""" definition = FORM_DEFINITIONS.get(form_type) if definition is None: raise ValueError(f"unknown form_type {form_type!r}") if fields is None: return {} if not isinstance(fields, dict): raise ValueError("fields must be an object") known = {f["key"]: f for f in definition["fields"]} enums = definition.get("field_enums", {}) normalized = {} for key, value in fields.items(): spec = known.get(key) if spec is None: continue if value in (None, ""): normalized[key] = None continue if key in enums: value = str(value).strip().lower() if value not in enums[key]: raise ValueError(f"{key} must be one of {', '.join(enums[key])}") elif spec["kind"] == "bool": if isinstance(value, str): value = value.strip().lower() in ("true", "yes", "1", "on") else: value = bool(value) else: value = str(value).strip() or None normalized[key] = value return normalized def combined_summary(rows): """Annexure E's OVERALL SCORE SUMMARY across the two evaluation forms. `rows` are candidate_forms records (attribute access: form_type, created_at, sections). The latest interview_analysis row supplies the technical and behavioral averages, the latest cultural_fit row the cultural average. The combined overall (mean of the three section averages, 2 dp) appears only once all three exist. Returns None when neither evaluation exists. """ latest = {} for row in rows: if row.form_type not in ("interview_analysis", "cultural_fit"): continue current = latest.get(row.form_type) if current is None or (row.created_at and current.created_at and row.created_at > current.created_at): latest[row.form_type] = row if not latest: return None averages = {"technical": None, "behavioral": None, "cultural": None} for row in latest.values(): for section in row.sections or []: key = section.get("key") if key in averages: averages[key] = section.get("average") complete = all(v is not None for v in averages.values()) return { "technical_avg": averages["technical"], "behavioral_avg": averages["behavioral"], "cultural_avg": averages["cultural"], "combined_overall": _mean(list(averages.values())) if complete else None, }