From 6f524361651cee341c87b70f1323a0827c4a6789 Mon Sep 17 00:00:00 2001 From: "ahmed.mujtaba" Date: Tue, 25 Aug 2026 20:18:12 +0500 Subject: [PATCH] . --- backend/g_sheet/enums.py | 48 +++++++++++++++++++++++---- backend/g_sheet/models.py | 11 ++++++ backend/g_sheet/plugins.py | 4 ++- backend/tests/test_g_sheet_plugins.py | 22 ++++++++++-- 4 files changed, 76 insertions(+), 9 deletions(-) diff --git a/backend/g_sheet/enums.py b/backend/g_sheet/enums.py index 0db456a..77a787e 100644 --- a/backend/g_sheet/enums.py +++ b/backend/g_sheet/enums.py @@ -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" diff --git a/backend/g_sheet/models.py b/backend/g_sheet/models.py index e63703c..d374d61 100644 --- a/backend/g_sheet/models.py +++ b/backend/g_sheet/models.py @@ -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 diff --git a/backend/g_sheet/plugins.py b/backend/g_sheet/plugins.py index 9d6645e..04fb1c2 100644 --- a/backend/g_sheet/plugins.py +++ b/backend/g_sheet/plugins.py @@ -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: diff --git a/backend/tests/test_g_sheet_plugins.py b/backend/tests/test_g_sheet_plugins.py index a8195a0..804dd13 100644 --- a/backend/tests/test_g_sheet_plugins.py +++ b/backend/tests/test_g_sheet_plugins.py @@ -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