295 lines
11 KiB
Python
295 lines
11 KiB
Python
"""Sheet header aliases, FormData keys, and date format mappings.
|
||
|
||
(str, Enum) like inbox/enums.py: members compare to and serialize as plain strings.
|
||
Non-string mappings (month pairs) use plain Enum.
|
||
"""
|
||
|
||
from enum import Enum
|
||
|
||
|
||
class AliasEnum(str, Enum):
|
||
"""Member-less base so alias enums share one `has` without 25 copies."""
|
||
|
||
@classmethod
|
||
def has(cls, value) -> bool:
|
||
return value in cls._value2member_map_
|
||
|
||
|
||
class FormDataField(str, Enum):
|
||
"""Canonical FormData column keys for the recruitment screening sheet."""
|
||
|
||
SERIAL_NO = "serial_no"
|
||
ENTRY_YEAR = "entry_year"
|
||
ENTRY_MONTH = "entry_month"
|
||
ENTRY_DATE = "entry_date"
|
||
ENTRY_TIME = "entry_time"
|
||
SCREENED_BY = "screened_by"
|
||
NAME = "name"
|
||
GENDER = "gender"
|
||
DATE_OF_BIRTH = "date_of_birth"
|
||
CNIC = "cnic"
|
||
CGPA = "cgpa"
|
||
HR_COMMENTS = "hr_comments"
|
||
CANDIDATE_NUMBER = "candidate_number"
|
||
CANDIDATE_EMAIL = "candidate_email"
|
||
PROFILE_LINK = "profile_link"
|
||
RESUME_LINK = "resume_link"
|
||
AREA_OF_EXPERTISE = "area_of_expertise"
|
||
REQUISITION_NUMBER = "requisition_number"
|
||
POSITION_APPLIED_FOR = "position_applied_for"
|
||
SOURCE_OF_APPLICATION = "source_of_application"
|
||
AGE = "age"
|
||
MARITAL_STATUS = "marital_status"
|
||
DEGREE = "degree"
|
||
UNIVERSITY = "university"
|
||
UNIVERSITY_OTHER = "university_other"
|
||
EXPERIENCE = "experience"
|
||
EXPERIENCE_DETAILS = "experience_details"
|
||
AREA_OF_RESIDENCE = "area_of_residence"
|
||
RESIDING_CITY = "residing_city"
|
||
RESIDING_COUNTRY = "residing_country"
|
||
COMMUNICATION_SKILLS = "communication_skills"
|
||
PREFERRED_TIMINGS = "preferred_timings"
|
||
HO_AVAILABILITY = "ho_availability"
|
||
CURRENT_COMPANY = "current_company"
|
||
REASON_FOR_LEAVING = "reason_for_leaving"
|
||
NOTICE_PERIOD = "notice_period"
|
||
CURRENT_SALARY = "current_salary"
|
||
EXPECTED_SALARY = "expected_salary"
|
||
DIRECTOR_POC_CATEGORY = "director_poc_category"
|
||
PROS = "pros"
|
||
CONS = "cons"
|
||
|
||
|
||
# canonical (lowercased, whitespace-collapsed, punctuation-stripped) header -> field.
|
||
# The sheet's own spelling is listed first; the rest are tolerated synonyms.
|
||
HEADER_ALIASES: dict[FormDataField, tuple[str, ...]] = {
|
||
FormDataField.SERIAL_NO: ("um", "sr", "sr no", "s no", "serial", "serial no"),
|
||
FormDataField.ENTRY_YEAR: ("year", "year of graduation"),
|
||
FormDataField.ENTRY_MONTH: ("month",),
|
||
FormDataField.ENTRY_DATE: ("date", "entry date", "date of entry", "timestamp", "time stamp"),
|
||
FormDataField.ENTRY_TIME: ("time of entry", "entry time", "time"),
|
||
FormDataField.SCREENED_BY: (
|
||
"screened by", "screened", "interviewed by", "conducted by", "recruiter",
|
||
),
|
||
FormDataField.NAME: (
|
||
"candidate name", "full name", "name", "names", "candidate",
|
||
),
|
||
FormDataField.GENDER: ("gender", "sex"),
|
||
FormDataField.DATE_OF_BIRTH: (
|
||
"date of birth", "dob", "birth date", "birthday",
|
||
),
|
||
FormDataField.CNIC: (
|
||
"national identification no", "national identification number",
|
||
"cnic", "nic", "national id", "cnic no", "cnic number",
|
||
),
|
||
FormDataField.CGPA: ("cgpa", "gpa", "grade point average"),
|
||
FormDataField.HR_COMMENTS: ("hr comments", "hr comment", "comments", "remarks"),
|
||
FormDataField.CANDIDATE_NUMBER: (
|
||
"candidate number", "contact number", "phone number", "phone", "mobile", "contact",
|
||
),
|
||
FormDataField.CANDIDATE_EMAIL: ("candidate email", "email", "email address"),
|
||
FormDataField.PROFILE_LINK: (
|
||
"profile link", "linkedin profile link", "linkedin", "profile",
|
||
),
|
||
FormDataField.RESUME_LINK: (
|
||
"drop your updated resume", "resume link", "cv link", "resume", "cv",
|
||
),
|
||
FormDataField.AREA_OF_EXPERTISE: (
|
||
"area of expertise", "area of interest", "expertise",
|
||
),
|
||
FormDataField.REQUISITION_NUMBER: ("requisition number", "requisition", "req no"),
|
||
FormDataField.POSITION_APPLIED_FOR: (
|
||
"position suitable for", "position applied for", "position",
|
||
"designation", "job title", "role", "title",
|
||
),
|
||
FormDataField.SOURCE_OF_APPLICATION: (
|
||
"source of application", "source", "application source",
|
||
"where did you hear about the position you're applying for",
|
||
),
|
||
FormDataField.AGE: ("age",),
|
||
FormDataField.MARITAL_STATUS: ("marital status", "marital", "family details"),
|
||
FormDataField.DEGREE: (
|
||
"education", "educational degree", "degree", "qualification",
|
||
),
|
||
FormDataField.UNIVERSITY: ("university of graduation", "university", "institute", "college"),
|
||
FormDataField.UNIVERSITY_OTHER: (
|
||
"if your university is not listed above, please specify its name",
|
||
"university other", "other university", "specify university",
|
||
),
|
||
FormDataField.EXPERIENCE: ("experience", "total experience", "years of experience", "exp"),
|
||
FormDataField.EXPERIENCE_DETAILS: ("experience details", "experience detail"),
|
||
FormDataField.AREA_OF_RESIDENCE: ("area of residence", "residence", "location", "address"),
|
||
FormDataField.RESIDING_CITY: ("residing city", "city"),
|
||
FormDataField.RESIDING_COUNTRY: ("residing country", "country"),
|
||
FormDataField.COMMUNICATION_SKILLS: ("communication skills", "communication"),
|
||
FormDataField.PREFERRED_TIMINGS: ("preferred timings", "preferred timing", "shift"),
|
||
FormDataField.HO_AVAILABILITY: (
|
||
"availability to work in the h.o", "availability to work in the ho",
|
||
"ho availability", "availability", "are you willing to relocate",
|
||
),
|
||
FormDataField.CURRENT_COMPANY: ("current company", "current employer", "company", "employer"),
|
||
FormDataField.REASON_FOR_LEAVING: ("reason for leaving", "reason of leaving", "reason"),
|
||
FormDataField.NOTICE_PERIOD: (
|
||
"how soon can you join us", "how soon can you join",
|
||
"notice period", "joining", "availability to join",
|
||
),
|
||
FormDataField.CURRENT_SALARY: ("current salary", "present salary", "salary"),
|
||
FormDataField.EXPECTED_SALARY: ("expected salary", "salary expectation", "expected"),
|
||
FormDataField.DIRECTOR_POC_CATEGORY: (
|
||
"director / poc / category", "director poc category",
|
||
"director / poc", "poc / category",
|
||
),
|
||
FormDataField.PROS: ("pros", "strengths"),
|
||
FormDataField.CONS: ("cons", "weaknesses"),
|
||
}
|
||
|
||
|
||
def _build_alias_to_field() -> dict[str, FormDataField]:
|
||
inverted: dict[str, FormDataField] = {}
|
||
for field, aliases in HEADER_ALIASES.items():
|
||
for alias in aliases:
|
||
if alias in inverted:
|
||
raise ValueError(
|
||
f"duplicate header alias {alias!r}: "
|
||
f"{inverted[alias].value} and {field.value}"
|
||
)
|
||
inverted[alias] = field
|
||
return inverted
|
||
|
||
|
||
ALIAS_TO_FIELD: dict[str, FormDataField] = _build_alias_to_field()
|
||
|
||
|
||
class FormDataColumn(str, Enum):
|
||
"""FormData API / ORM field names in serialize order.
|
||
|
||
Broader than FormDataField: includes id, sheet meta, derived parsers
|
||
(age_raw, *_salary_value), raw_record, and timestamps.
|
||
"""
|
||
|
||
ID = "id"
|
||
SHEET = "sheet"
|
||
JOB_POST_ID = "job_post_id"
|
||
ASSIGNED_JOB_POST_ID = "assigned_job_post_id"
|
||
SUGGESTED_JOB_POST_IDS = "suggested_job_post_ids"
|
||
MANUAL_UPLOAD_CANDIDATE_ID = "manual_upload_candidate_id"
|
||
ROW_NUMBER = "row_number"
|
||
SERIAL_NO = "serial_no"
|
||
ENTRY_YEAR = "entry_year"
|
||
ENTRY_MONTH = "entry_month"
|
||
ENTRY_DATE = "entry_date"
|
||
ENTRY_TIME = "entry_time"
|
||
SCREENED_BY = "screened_by"
|
||
NAME = "name"
|
||
GENDER = "gender"
|
||
DATE_OF_BIRTH = "date_of_birth"
|
||
CNIC = "cnic"
|
||
CGPA = "cgpa"
|
||
HR_COMMENTS = "hr_comments"
|
||
CANDIDATE_NUMBER = "candidate_number"
|
||
CANDIDATE_EMAIL = "candidate_email"
|
||
PROFILE_LINK = "profile_link"
|
||
RESUME_LINK = "resume_link"
|
||
EXTRACTED_DATA = "extracted_data"
|
||
AREA_OF_EXPERTISE = "area_of_expertise"
|
||
REQUISITION_NUMBER = "requisition_number"
|
||
POSITION_APPLIED_FOR = "position_applied_for"
|
||
SOURCE_OF_APPLICATION = "source_of_application"
|
||
AGE = "age"
|
||
AGE_RAW = "age_raw"
|
||
MARITAL_STATUS = "marital_status"
|
||
DEGREE = "degree"
|
||
UNIVERSITY = "university"
|
||
UNIVERSITY_OTHER = "university_other"
|
||
EXPERIENCE = "experience"
|
||
EXPERIENCE_DETAILS = "experience_details"
|
||
AREA_OF_RESIDENCE = "area_of_residence"
|
||
RESIDING_CITY = "residing_city"
|
||
CITY = "city"
|
||
RESIDING_COUNTRY = "residing_country"
|
||
COMMUNICATION_SKILLS = "communication_skills"
|
||
PREFERRED_TIMINGS = "preferred_timings"
|
||
HO_AVAILABILITY = "ho_availability"
|
||
CURRENT_COMPANY = "current_company"
|
||
REASON_FOR_LEAVING = "reason_for_leaving"
|
||
NOTICE_PERIOD = "notice_period"
|
||
CURRENT_SALARY = "current_salary"
|
||
CURRENT_SALARY_VALUE = "current_salary_value"
|
||
EXPECTED_SALARY = "expected_salary"
|
||
EXPECTED_SALARY_VALUE = "expected_salary_value"
|
||
DIRECTOR_POC_CATEGORY = "director_poc_category"
|
||
PROS = "pros"
|
||
CONS = "cons"
|
||
# Same vocabulary as inbox_messages — Import / Shortlist / Reject / Duplicate.
|
||
PROCESSING_STATE = "processing_state"
|
||
IS_DUPLICATE = "is_duplicate"
|
||
REAPPLIED = "reapplied"
|
||
RAW_RECORD = "raw_record"
|
||
IMPORTED_AT = "imported_at"
|
||
CREATED_AT = "created_at"
|
||
UPDATED_AT = "updated_at"
|
||
|
||
|
||
# Ordered values for serialize_form_data / model_fields assertions.
|
||
FORM_DATA_FIELDS: tuple[str, ...] = tuple(member.value for member in FormDataColumn)
|
||
|
||
|
||
# -- Date parsing ------------------------------------------------------------
|
||
|
||
class DateFormat(str, Enum):
|
||
"""strptime patterns tried in definition order after numeric slash dates.
|
||
|
||
Numeric D/M vs M/D is resolved in parse_date (8/28 → Aug 28, 28/8 → 28 Aug,
|
||
8/12 follows prefer_mdy). These patterns cover named months and ISO.
|
||
"""
|
||
|
||
D_MON_Y_DASH = "%d-%b-%Y"
|
||
D_MONTH_Y_DASH = "%d-%B-%Y"
|
||
D_MON_Y_SPACE = "%d %b %Y"
|
||
D_MONTH_Y_SPACE = "%d %B %Y"
|
||
DMY_SLASH = "%d/%m/%Y"
|
||
DMY_SLASH_SHORT = "%d/%m/%y"
|
||
DMY_DASH = "%d-%m-%Y"
|
||
DMY_DASH_SHORT = "%d-%m-%y"
|
||
ISO = "%Y-%m-%d"
|
||
DMY_DOT = "%d.%m.%Y"
|
||
DMY_DOT_SHORT = "%d.%m.%y"
|
||
MDY_SLASH = "%m/%d/%Y"
|
||
MDY_SLASH_SHORT = "%m/%d/%y"
|
||
MON_D_Y = "%b %d %Y"
|
||
MONTH_D_Y = "%B %d %Y"
|
||
D_MON_Y_SHORT = "%d-%b-%y"
|
||
D_MON_Y_SPACE_SHORT = "%d %b %y"
|
||
D_MON_Y_SLASH = "%d/%b/%Y"
|
||
D_MON_Y_SLASH_SHORT = "%d/%b/%y"
|
||
|
||
|
||
class DateTimeSeparator(str, Enum):
|
||
"""Separators that split a date cell into date + time tails."""
|
||
|
||
DASH = " - "
|
||
EN_DASH = " – "
|
||
EM_DASH = " — "
|
||
SLASH_SPACE = "/ "
|
||
PIPE = " | "
|
||
|
||
|
||
class MonthNormalisation(Enum):
|
||
"""Sheet month spellings → %b-safe short form. value is (source, short)."""
|
||
|
||
SEPTEMBER = ("september", "sep")
|
||
SEPT = ("sept", "sep")
|
||
JULY = ("july", "jul")
|
||
JUNE = ("june", "jun")
|
||
APRIL = ("april", "apr")
|
||
MARCH = ("march", "mar")
|
||
|
||
@property
|
||
def source(self) -> str:
|
||
return self.value[0]
|
||
|
||
@property
|
||
def short(self) -> str:
|
||
return self.value[1]
|