"""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" PROFESSIONAL_SUMMARY = "professional_summary" 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]