diff --git a/README.md b/README.md index 50d0976..a0e2d77 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # PDF Rename Sort -A Python web application built with Flask that automatically renames PDF files based on form field data extracted from within the PDFs. Designed for batch processing of fillable PDFs, extracting "Last Name" and "First Name" to create standardized filenames. +A Python web application built with Flask that automatically renames PDF files based on form field data extracted from within the PDFs and organizes them into a structured folder hierarchy. Designed for batch processing of fillable PDFs, extracting student information and creating organized archives. ## Features @@ -8,8 +8,9 @@ A Python web application built with Flask that automatically renames PDF files b - **Batch Processing**: Handle multiple PDFs or ZIP archives containing PDFs - **Form Field Extraction**: Uses PyPDF2 to read PDF form fields - **Intelligent Renaming**: Renames files to "LastName_FirstName.pdf" format +- **Structured Organization**: Creates Episode/School/Grade folder hierarchy - **Duplicate Handling**: Automatically appends counters for duplicate names -- **ZIP Support**: Upload ZIP files and download processed results as ZIP +- **ZIP Support**: Upload ZIP files and download processed results as ZIP with folder structure - **Job Management**: Background processing with job status tracking and history - **Authentication**: Microsoft Entra ID and local admin login support - **Error Handling**: Gracefully skips files that can't be processed @@ -102,17 +103,31 @@ Users can choose their preferred login method on the login page. 1. **File Upload**: Accepts PDFs or ZIPs via web form 2. **Extraction**: If ZIP, extracts PDFs to temporary directory -3. **Processing**: For each PDF, reads form fields using PyPDF2 -4. **Renaming**: Extracts "Last Name" and "First Name" values -5. **Packaging**: Creates a ZIP archive of renamed PDFs -6. **Download**: Provides secure download link for results +3. **Field Parsing**: For each PDF, reads form fields using PyPDF2: + - Student First Name + - Student Last Name + - Student Grade + - Student School +4. **Renaming**: Creates filename as "LastName_FirstName.pdf" +5. **Organization**: Creates folder structure: + ``` + Episode/ + ├── School Name/ + │ ├── Grade Level/ + │ │ ├── LastName_FirstName.pdf + │ │ └── LastName_FirstName_2.pdf + ``` +6. **Packaging**: Creates a ZIP archive preserving the folder structure +7. **Download**: Provides secure download link for the organized results ## Important Notes -- PDFs must contain fillable form fields named exactly "Last Name" and "First Name" +- PDFs must contain fillable form fields named exactly: "Student First Name", "Student Last Name", "Student Grade", and "Student School" +- All four fields must be present and filled for the PDF to be processed - Files without these fields or with extraction errors will be skipped - All processing happens server-side with temporary file storage - Maximum upload size is limited to 500MB +- Output creates a structured folder hierarchy within the downloaded ZIP ## Production Deployment diff --git a/pdf_renamer.py b/pdf_renamer.py index efed51e..14fc4e4 100644 --- a/pdf_renamer.py +++ b/pdf_renamer.py @@ -52,10 +52,21 @@ def extract_names_from_pdf(pdf_path): try: reader = PdfReader(pdf_path) fields = reader.get_fields() - last_name = fields.get('Last Name', {}).get('/V', '') - first_name = fields.get('First Name', {}).get('/V', '') - if last_name and first_name: - return f"{last_name}_{first_name}.pdf" + + # Extract all required fields + last_name = fields.get('Last Name', {}).get('/V', '').strip() + first_name = fields.get('First Name', {}).get('/V', '').strip() + grade = fields.get('Student Grade', {}).get('/V', '').strip() + school = fields.get('Student School', {}).get('/V', '').strip() + + if last_name and first_name and grade and school: + return { + 'filename': f"{last_name}_{first_name}.pdf", + 'last_name': last_name, + 'first_name': first_name, + 'grade': grade, + 'school': school + } else: return None except Exception as e: @@ -66,6 +77,10 @@ def process_job(job_id, files): processed_files = [] temp_extract_dirs = [] + # Create base Episode directory for this job + episode_dir = os.path.join(app.config['UPLOAD_FOLDER'], f'Episode_{job_id}') + os.makedirs(episode_dir, exist_ok=True) + for file_info in files: filepath = file_info['path'] filename = file_info['name'] @@ -82,40 +97,55 @@ def process_job(job_id, files): for f in files_in_zip: if f.lower().endswith('.pdf'): pdf_path = os.path.join(root, f) - new_name = extract_names_from_pdf(pdf_path) - if new_name: - new_path = os.path.join(root, new_name) + pdf_info = extract_names_from_pdf(pdf_path) + if pdf_info: + # Create folder structure: Episode/School/Grade/ + school_dir = os.path.join(episode_dir, pdf_info['school']) + grade_dir = os.path.join(school_dir, pdf_info['grade']) + os.makedirs(grade_dir, exist_ok=True) + + # Move file to appropriate folder with new name + new_path = os.path.join(grade_dir, pdf_info['filename']) counter = 1 while os.path.exists(new_path): - base, ext = os.path.splitext(new_name) - new_path = os.path.join(root, f"{base}_{counter}{ext}") + base, ext = os.path.splitext(pdf_info['filename']) + new_path = os.path.join(grade_dir, f"{base}_{counter}{ext}") counter += 1 os.rename(pdf_path, new_path) - processed_files.append(('success', os.path.relpath(new_path, extract_dir), new_path)) + processed_files.append(('success', os.path.relpath(new_path, episode_dir), new_path)) else: processed_files.append(('skipped', f, None)) else: # Single PDF - new_name = extract_names_from_pdf(filepath) - if new_name: - new_filepath = os.path.join(app.config['UPLOAD_FOLDER'], f"{job_id}_{new_name}") + pdf_info = extract_names_from_pdf(filepath) + if pdf_info: + # Create folder structure: Episode/School/Grade/ + school_dir = os.path.join(episode_dir, pdf_info['school']) + grade_dir = os.path.join(school_dir, pdf_info['grade']) + os.makedirs(grade_dir, exist_ok=True) + + # Move file to appropriate folder with new name + new_filepath = os.path.join(grade_dir, pdf_info['filename']) counter = 1 while os.path.exists(new_filepath): - base, ext = os.path.splitext(new_name) - new_filepath = os.path.join(app.config['UPLOAD_FOLDER'], f"{job_id}_{base}_{counter}{ext}") + base, ext = os.path.splitext(pdf_info['filename']) + new_filepath = os.path.join(grade_dir, f"{base}_{counter}{ext}") counter += 1 os.rename(filepath, new_filepath) - processed_files.append(('success', os.path.basename(new_filepath), new_filepath)) + processed_files.append(('success', os.path.relpath(new_filepath, episode_dir), new_filepath)) else: processed_files.append(('skipped', filename, None)) - # Create ZIP of all successful renames + # Create ZIP of the entire Episode directory structure successful_files = [p for p in processed_files if p[0] == 'success'] if successful_files: zip_output = os.path.join(app.config['UPLOAD_FOLDER'], f'renamed_{job_id}.zip') with zipfile.ZipFile(zip_output, 'w') as zip_out: - for _, rel_name, full_path in successful_files: - zip_out.write(full_path, rel_name) + for root, dirs, files_in_dir in os.walk(episode_dir): + for file in files_in_dir: + file_path = os.path.join(root, file) + arcname = os.path.relpath(file_path, episode_dir) + zip_out.write(file_path, arcname) job_queue[job_id]['zip_path'] = zip_output job_queue[job_id]['zip_name'] = f'renamed_{job_id}.zip'