Skip to content

Commit e33924a

Browse files
author
leyangloh
committed
fix convert HAAG encrollment script
1 parent 975f56c commit e33924a

1 file changed

Lines changed: 193 additions & 18 deletions

File tree

convert_HAAG_enrollment_to_qualtrics.py

Lines changed: 193 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,76 @@
44
from datetime import datetime
55

66
def read_excel_data(filepath):
7-
"""Read the Excel file and return structured data"""
8-
df = pd.read_excel(filepath, sheet_name='AggrigatedLabs')
7+
"""Read the Excel file and return structured data from individual lab sheets"""
8+
xl_file = pd.ExcelFile(filepath)
9+
10+
# Sheets to ignore
11+
ignore_sheets = ['CS8903', 'CS6999', 'Sheet1', 'AggrigatedLabs']
12+
13+
# Get all lab sheets (sheet name = lab name)
14+
lab_sheets = [s for s in xl_file.sheet_names if s not in ignore_sheets]
915

10-
# Group by Lab and Project
1116
structure = {}
12-
for _, row in df.iterrows():
13-
lab = row['Lab']
14-
project = row['Project']
15-
student = row['Student']
17+
18+
for lab_name in lab_sheets:
19+
df = pd.read_excel(filepath, sheet_name=lab_name)
20+
21+
# Find the Student column (might be 'Student' or 'Student ' with trailing space)
22+
student_col = None
23+
for col in df.columns:
24+
if 'Student' in str(col):
25+
student_col = col
26+
break
27+
28+
if student_col is None:
29+
print(f"Warning: No Student column found in sheet '{lab_name}', skipping")
30+
continue
31+
32+
# Find the Project column
33+
project_col = None
34+
for col in df.columns:
35+
if 'Project' in str(col):
36+
project_col = col
37+
break
1638

17-
if pd.isna(lab) or pd.isna(project) or pd.isna(student):
39+
if project_col is None:
40+
print(f"Warning: No Project column found in sheet '{lab_name}', skipping")
1841
continue
42+
43+
# Forward-fill the Project column (project name is usually only in first row of group)
44+
df[project_col] = df[project_col].ffill()
45+
46+
# Filter out rows with missing students
47+
df = df[df[student_col].notna()]
48+
49+
# Initialize lab in structure
50+
if lab_name not in structure:
51+
structure[lab_name] = {}
52+
53+
# Group by Project and collect students
54+
for _, row in df.iterrows():
55+
project = row[project_col]
56+
student = row[student_col]
57+
58+
# Skip rows with missing project or student
59+
if pd.isna(project) or pd.isna(student):
60+
continue
61+
62+
# Convert to string and strip whitespace
63+
project = str(project).strip()
64+
student = str(student).strip()
65+
66+
# Skip empty strings
67+
if not project or not student:
68+
continue
69+
70+
# Initialize project in lab structure
71+
if project not in structure[lab_name]:
72+
structure[lab_name][project] = []
1973

20-
if lab not in structure:
21-
structure[lab] = {}
22-
if project not in structure[lab]:
23-
structure[lab][project] = []
24-
structure[lab][project].append(student)
74+
# Only add if student name is not already in the list (avoid duplicates)
75+
if student not in structure[lab_name][project]:
76+
structure[lab_name][project].append(student)
2577

2678
return structure
2779

@@ -33,10 +85,13 @@ def generate_qualtrics_qsf(data):
3385
response_set_id = "RS_" + datetime.now().strftime("%Y%m%d%H%M%S")
3486

3587
# Base survey structure
88+
# Extract semester from data if available
89+
semester = data.get('_semester', 'Fall 2025')
90+
3691
survey = {
3792
"SurveyEntry": {
3893
"SurveyID": survey_id,
39-
"SurveyName": "HAAG Fall 2025 - Weekly Research Team Progress Check",
94+
"SurveyName": f"HAAG {semester} - Weekly Research Team Progress Check",
4095
"SurveyDescription": None,
4196
"SurveyOwnerID": "UR_XXXXXXXXXXXXX",
4297
"SurveyBrandID": "gatech",
@@ -118,7 +173,9 @@ def generate_qualtrics_qsf(data):
118173

119174
# Create choices for labs
120175
lab_choices = {}
121-
lab_names = sorted(data.keys())
176+
# Filter out non-lab keys (like _semester)
177+
lab_data = {k: v for k, v in data.items() if isinstance(v, dict)}
178+
lab_names = sorted(lab_data.keys())
122179
for idx, lab in enumerate(lab_names, 1):
123180
lab_choices[str(idx)] = {
124181
"Display": lab
@@ -163,7 +220,7 @@ def generate_qualtrics_qsf(data):
163220
block_counter = 2 # Start from BL_2 since BL_1 is the lab selection block
164221

165222
for lab_idx, lab_name in enumerate(lab_names, 1):
166-
projects = data[lab_name]
223+
projects = lab_data[lab_name]
167224

168225
# Create a new block for this lab
169226
lab_block_id = f"BL_{block_counter}"
@@ -320,6 +377,107 @@ def generate_qualtrics_qsf(data):
320377
blocks[str(block_counter - 1)] = lab_block
321378
block_counter += 1
322379

380+
# Create "Overall Check" block with final questions
381+
# Calculate block ID: BL_1 is initial, BL_2 through BL_(len+1) are labs, so overall is BL_(len+2)
382+
overall_block_id = f"BL_{len(lab_names) + 2}"
383+
overall_block = {
384+
"Type": "Standard",
385+
"Description": "Overall Check",
386+
"ID": overall_block_id,
387+
"BlockElements": [],
388+
"Options": {
389+
"BlockLocking": "false",
390+
"RandomizeQuestions": "false",
391+
"BlockVisibility": "Expanded"
392+
}
393+
}
394+
395+
# Q: Overall progress toward publication
396+
q_overall_id = f"QID{qid_counter}"
397+
qid_counter += 1
398+
overall_block["BlockElements"].append({"Type": "Question", "QuestionID": q_overall_id})
399+
400+
questions.append({
401+
"SurveyID": survey_id,
402+
"Element": "SQ",
403+
"PrimaryAttribute": q_overall_id,
404+
"SecondaryAttribute": "How do you evaluate the team's overall progress toward publication?",
405+
"TertiaryAttribute": None,
406+
"Payload": {
407+
"QuestionText": "How do you evaluate the team's overall progress toward publication?",
408+
"DataExportTag": "Q_Overall_Progress",
409+
"QuestionID": q_overall_id,
410+
"QuestionType": "MC",
411+
"Selector": "SAHR",
412+
"SubSelector": "TX",
413+
"Configuration": {
414+
"QuestionDescriptionOption": "SpecifyLabel",
415+
"TextPosition": "inline",
416+
"LabelPosition": "BELOW"
417+
},
418+
"QuestionDescription": "How do you evaluate the team's overall progress toward publication?",
419+
"Choices": {
420+
"1": {"Display": "On Track"},
421+
"2": {"Display": "Needs Improvement"},
422+
"3": {"Display": "Blocked"}
423+
},
424+
"ChoiceOrder": [1, "2", "3"],
425+
"Validation": {
426+
"Settings": {
427+
"ForceResponse": "ON",
428+
"ForceResponseType": "ON",
429+
"Type": "None"
430+
}
431+
},
432+
"GradingData": [],
433+
"Language": [],
434+
"NextChoiceId": 4,
435+
"NextAnswerId": 4
436+
}
437+
})
438+
439+
# Q: Anything else you'd like to share
440+
q_comments_id = f"QID{qid_counter}"
441+
qid_counter += 1
442+
overall_block["BlockElements"].append({"Type": "Question", "QuestionID": q_comments_id})
443+
444+
questions.append({
445+
"SurveyID": survey_id,
446+
"Element": "SQ",
447+
"PrimaryAttribute": q_comments_id,
448+
"SecondaryAttribute": "Anything else you'd like to share about this team's performance and progress?",
449+
"TertiaryAttribute": None,
450+
"Payload": {
451+
"QuestionText": "Anything else you'd like to share about this team's performance and progress?<i> (blockers, concerns, or positive notes)</i><br>",
452+
"DataExportTag": "Q_Comments",
453+
"QuestionID": q_comments_id,
454+
"QuestionType": "TE",
455+
"Selector": "ML",
456+
"Configuration": {
457+
"QuestionDescriptionOption": "UseText"
458+
},
459+
"QuestionDescription": "Anything else you'd like to share about this team's performance and progress? (blockers, concerns...)",
460+
"Validation": {
461+
"Settings": {
462+
"ForceResponse": "OFF",
463+
"Type": "None"
464+
}
465+
},
466+
"GradingData": [],
467+
"Language": [],
468+
"NextChoiceId": 4,
469+
"NextAnswerId": 1,
470+
"SearchSource": {
471+
"AllowFreeResponse": "false"
472+
}
473+
}
474+
})
475+
476+
# Store the overall check block
477+
# After n labs, blocks["0"] through blocks[str(n-1)] are used, and block_counter = n+1
478+
# So we use blocks[str(block_counter - 1)] which is blocks[str(n)]
479+
blocks[str(block_counter - 1)] = overall_block
480+
323481
# Build SurveyElements in the correct order
324482
# 1. Blocks element (BL) - Payload is a dict, not array
325483
survey["SurveyElements"].append({
@@ -349,6 +507,15 @@ def generate_qualtrics_qsf(data):
349507
"FlowID": f"FL_{lab_idx + 10}",
350508
"Autofill": []
351509
})
510+
511+
# Add the Overall Check block at the end
512+
overall_block_id = f"BL_{len(lab_names) + 2}"
513+
flow_elements.append({
514+
"Type": "Standard",
515+
"ID": overall_block_id,
516+
"FlowID": f"FL_{len(lab_names) + 100}",
517+
"Autofill": []
518+
})
352519

353520
survey_flow = {
354521
"Type": "Root",
@@ -472,21 +639,29 @@ def generate_qualtrics_qsf(data):
472639
def main():
473640
"""Main function to convert Excel to QSF"""
474641

642+
# Prompt user for semester
643+
semester = input("Enter the semester (e.g., 'Fall 2025', 'Spring 2026'): ").strip()
644+
if not semester:
645+
semester = "Fall 2025" # Default
646+
print(f"Using default: {semester}")
647+
475648
# Read Excel file
476649
print("Reading Excel file...")
477650
excel_file = "HAAG_Fall_Enrollment_Students.xlsx"
478651
data = read_excel_data(excel_file)
479652

480653
print(f"Found {len(data)} labs")
481654
for lab, projects in data.items():
482-
print(f" {lab}: {len(projects)} projects")
655+
if isinstance(projects, dict): # Skip non-dict entries like _semester
656+
print(f" {lab}: {len(projects)} projects")
483657

484658
# Generate QSF
485659
print("\nGenerating Qualtrics QSF file...")
660+
data['_semester'] = semester # Pass semester to generation function
486661
survey = generate_qualtrics_qsf(data)
487662

488663
# Save to file
489-
output_file = "HAAG_Fall_2025_Survey.qsf"
664+
output_file = f"HAAG_{semester.replace(' ', '_')}_Survey.qsf"
490665
with open(output_file, 'w', encoding='utf-8') as f:
491666
json.dump(survey, f, indent=2, ensure_ascii=False)
492667

0 commit comments

Comments
 (0)