pull/29/head
ahmed.mujtaba 2026-08-25 20:18:12 +05:00
parent ddfbb00edf
commit 6f52436165
4 changed files with 76 additions and 9 deletions

View File

@ -25,10 +25,15 @@ class FormDataField(str, Enum):
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_SUITABLE_FOR = "position_suitable_for"
@ -37,9 +42,12 @@ class FormDataField(str, Enum):
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"
@ -48,6 +56,7 @@ class FormDataField(str, Enum):
NOTICE_PERIOD = "notice_period"
CURRENT_SALARY = "current_salary"
EXPECTED_SALARY = "expected_salary"
DIRECTOR_POC_CATEGORY = "director_poc_category"
PROS = "pros"
CONS = "cons"
@ -66,14 +75,25 @@ HEADER_ALIASES: dict[FormDataField, tuple[str, ...]] = {
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", "cv link", "resume link",
"drop your updated resume", "profile",
"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",
@ -93,12 +113,15 @@ HEADER_ALIASES: dict[FormDataField, tuple[str, ...]] = {
"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", "residing city", "residing country",
"residence", "location", "address",
),
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: (
@ -113,6 +136,10 @@ HEADER_ALIASES: dict[FormDataField, tuple[str, ...]] = {
),
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"),
}
@ -152,10 +179,15 @@ class FormDataColumn(str, Enum):
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_SUITABLE_FOR = "position_suitable_for"
@ -165,9 +197,12 @@ class FormDataColumn(str, Enum):
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"
@ -178,6 +213,7 @@ class FormDataColumn(str, Enum):
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"
RAW_RECORD = "raw_record"

View File

@ -39,10 +39,15 @@ class FormData(SQLModel, table=True):
entry_time: str | None = Field(default=None)
screened_by: str | None = Field(default=None, index=True)
name: str | None = Field(default=None, index=True)
gender: str | None = Field(default=None)
date_of_birth: datetime | None = Field(default=None, sa_type=DateTime(timezone=True))
cnic: str | None = Field(default=None, index=True)
cgpa: str | None = Field(default=None)
hr_comments: str | None = Field(default=None)
candidate_number: str | None = Field(default=None)
candidate_email: str | None = Field(default=None, index=True)
profile_link: str | None = Field(default=None)
resume_link: str | None = Field(default=None)
area_of_expertise: str | None = Field(default=None)
requisition_number: str | None = Field(default=None, index=True)
position_suitable_for: str | None = Field(default=None)
@ -52,9 +57,12 @@ class FormData(SQLModel, table=True):
marital_status: str | None = Field(default=None)
degree: str | None = Field(default=None)
university: str | None = Field(default=None)
university_other: str | None = Field(default=None)
experience: str | None = Field(default=None)
experience_details: str | None = Field(default=None)
area_of_residence: str | None = Field(default=None)
residing_city: str | None = Field(default=None)
residing_country: str | None = Field(default=None)
communication_skills: int | None = Field(default=None)
preferred_timings: str | None = Field(default=None)
ho_availability: str | None = Field(default=None)
@ -65,6 +73,7 @@ class FormData(SQLModel, table=True):
current_salary_value: int | None = Field(default=None)
expected_salary: str | None = Field(default=None)
expected_salary_value: int | None = Field(default=None)
director_poc_category: str | None = Field(default=None)
pros: str | None = Field(default=None)
cons: str | None = Field(default=None)
@ -96,6 +105,8 @@ class FormData(SQLModel, table=True):
cls.position_suitable_for.ilike(pattern),
cls.area_of_expertise.ilike(pattern),
cls.source_of_application.ilike(pattern),
cls.cnic.ilike(pattern),
cls.residing_city.ilike(pattern),
))
return filters

View File

@ -256,7 +256,7 @@ _CURRENCY_STRIP_RE=re.compile(r"(?:rs\.?|pkr|inr|usd|\$|€|£)",re.I)
# Every typed column key the mapper must emit (uniform dicts for bulk insert).
_FORM_DATA_COLUMN_KEYS=tuple(field.value for field in FormDataField)+(
"age_raw","current_salary_value","expected_salary_value",
"age_raw","current_salary_value","expected_salary_value","job_post_id",
)
@ -453,6 +453,8 @@ def map_record_to_form_data(sheet,record,headers,row_number):
mapped["entry_date"]=dt
if tm and not mapped.get("entry_time"):
mapped["entry_time"]=tm
elif field==FormDataField.DATE_OF_BIRTH:
mapped["date_of_birth"]=parse_date(value)
elif field==FormDataField.COMMUNICATION_SKILLS:
mapped["communication_skills"]=parse_score(value)
elif field==FormDataField.CURRENT_SALARY:

View File

@ -220,8 +220,11 @@ FORM_RESPONSE_HEADERS = [
"Email",
"Full Name",
"Gender",
"Date of Birth",
"Marital Status",
"CGPA",
"University",
"If your university is not listed above, please specify its name.",
"Educational Degree",
"Year of Graduation",
"Position Applied For",
@ -234,6 +237,8 @@ FORM_RESPONSE_HEADERS = [
"Residing Country",
"Area of Interest",
"Recruiter",
"Director / POC / Category",
"National Identification No. (42000-XXXXXXX-X)",
"Where did you hear about the position you're applying for?",
]
@ -242,8 +247,11 @@ FORM_RESPONSE_RECORD = {
"Email": "nusratazra@gmail.com",
"Full Name": "Nusrat Azra",
"Gender": "Female",
"Date of Birth": "7/24/1984",
"Marital Status": "Single",
"CGPA": "3.5",
"University": "Karachi University",
"If your university is not listed above, please specify its name.": "",
"Educational Degree": "Masters",
"Year of Graduation": "12/2/2007",
"Position Applied For": "Executive Secretary",
@ -256,11 +264,14 @@ FORM_RESPONSE_RECORD = {
"Residing Country": "Pakistan",
"Area of Interest": "",
"Recruiter": "",
"Director / POC / Category": "Operations",
"National Identification No. (42000-XXXXXXX-X)": "4250105627772",
"Where did you hear about the position you're applying for?": "Indeed",
}
def test_form_response_headers_map_to_typed_columns():
assert plugins.collect_unmapped_headers(FORM_RESPONSE_HEADERS) == []
mapped = plugins.map_record_to_form_data(
"Form Responses - Candidate Database Sheet 2026",
FORM_RESPONSE_RECORD,
@ -270,6 +281,10 @@ def test_form_response_headers_map_to_typed_columns():
assert mapped["name"] == "Nusrat Azra"
assert mapped["candidate_email"] == "nusratazra@gmail.com"
assert mapped["candidate_number"] == "03312464228"
assert mapped["gender"] == "Female"
assert mapped["date_of_birth"] == datetime(1984, 7, 24, tzinfo=timezone.utc)
assert mapped["cnic"] == "4250105627772"
assert mapped["cgpa"] == "3.5"
assert mapped["degree"] == "Masters"
assert mapped["university"] == "Karachi University"
assert mapped["position_suitable_for"] == "Executive Secretary"
@ -277,13 +292,16 @@ def test_form_response_headers_map_to_typed_columns():
assert mapped["source_of_application"] == "Indeed"
assert mapped["ho_availability"] == "Yes"
assert mapped["marital_status"] == "Single"
assert mapped["area_of_residence"] == "Karachi" # first matching residence header wins
assert mapped["residing_city"] == "Karachi"
assert mapped["residing_country"] == "Pakistan"
assert mapped["director_poc_category"] == "Operations"
assert mapped["profile_link"] == (
"https://www.linkedin.com/in/nusrat-azra-executive-manager-to-c-suite-3bb86719/"
)
assert mapped["resume_link"] == "https://drive.google.com/open?id=1l5HOY5R6KiL6270A_sV56FkdX6EIGfI4"
assert mapped["entry_year"] == "12/2/2007"
assert mapped["entry_date"] is not None
assert mapped["entry_time"] == "17:50:19"
assert mapped["entry_time"] == "17:50"
# Timestamp must never be used as the candidate name.
assert mapped["name"] != "7/2/2026 17:50:19"
assert mapped["job_post_id"] is None