363 lines
15 KiB
Python
363 lines
15 KiB
Python
"""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,
|
|
}
|