import os import tempfile import zipfile import threading import uuid from functools import wraps from flask import Flask, request, render_template_string, redirect, url_for, session, send_from_directory from werkzeug.utils import secure_filename from PyPDF2 import PdfReader from authlib.integrations.flask_client import OAuth app = Flask(__name__) app.config['UPLOAD_FOLDER'] = tempfile.mkdtemp() app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024 # 500MB limit app.secret_key = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production') # Entra ID Configuration TENANT_ID = os.getenv('TENANT_ID', 'your-tenant-id') CLIENT_ID = os.getenv('CLIENT_ID', 'your-client-id') CLIENT_SECRET = os.getenv('CLIENT_SECRET', 'your-client-secret') # Admin Configuration ADMIN_USERNAME = os.getenv('ADMIN_USERNAME', 'trbcit') ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD', 'atdrmbicn') oauth = OAuth(app) microsoft = oauth.register( name='microsoft', client_id=CLIENT_ID, client_secret=CLIENT_SECRET, server_metadata_url=f'https://login.microsoftonline.com/{TENANT_ID}/v2.0/.well-known/openid_configuration', client_kwargs={'scope': 'openid profile email'} ) ALLOWED_EXTENSIONS = {'pdf', 'zip'} # Global jobs storage (in production, use database) job_queue = {} def login_required(f): @wraps(f) def decorated_function(*args, **kwargs): if 'user' not in session and 'admin' not in session: return redirect(url_for('login')) return f(*args, **kwargs) return decorated_function def allowed_file(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS def extract_names_from_pdf(pdf_path): try: reader = PdfReader(pdf_path) fields = reader.get_fields() # 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() # Check for missing fields missing_fields = [] if not last_name: missing_fields.append('Last Name') if not first_name: missing_fields.append('First Name') if not grade: missing_fields.append('Student Grade') if not school: missing_fields.append('Student School') if missing_fields: return {'error': f'Missing or empty fields: {", ".join(missing_fields)}'} return { 'filename': f"{last_name}_{first_name}.pdf", 'last_name': last_name, 'first_name': first_name, 'grade': grade, 'school': school } except Exception as e: return {'error': f'PDF read error: {str(e)}'} def process_job(job_id, files): job_queue[job_id]['status'] = 'processing' job_queue[job_id]['progress'] = 'Starting job processing...' 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) job_queue[job_id]['progress'] = 'Created output directory structure' # Analyze ZIP contents before processing zip_contents = {} for file_info in files: if file_info['name'].lower().endswith('.zip'): try: with zipfile.ZipFile(file_info['path'], 'r') as zip_ref: pdf_files = [f for f in zip_ref.namelist() if f.lower().endswith('.pdf')] zip_contents[file_info['name']] = pdf_files except Exception as e: zip_contents[file_info['name']] = [f"Error reading ZIP: {str(e)}"] job_queue[job_id]['zip_contents'] = zip_contents total_files = sum(1 for file_info in files if file_info['name'].lower().endswith('.pdf')) + \ sum(len(zip_contents.get(file_info['name'], [])) for file_info in files if file_info['name'].lower().endswith('.zip')) processed_count = 0 processed_count = 0 for file_info in files: filepath = file_info['path'] filename = file_info['name'] if filename.lower().endswith('.zip'): job_queue[job_id]['progress'] = f'Extracting ZIP archive: {filename}' # Extract ZIP extract_dir = os.path.join(app.config['UPLOAD_FOLDER'], f'extract_{job_id}_{os.path.splitext(filename)[0]}') os.makedirs(extract_dir, exist_ok=True) temp_extract_dirs.append(extract_dir) with zipfile.ZipFile(filepath, 'r') as zip_ref: zip_ref.extractall(extract_dir) job_queue[job_id]['progress'] = f'Extracted {len(zip_ref.namelist())} files from {filename}' # Process PDFs in extract_dir for root, dirs, files_in_zip in os.walk(extract_dir): for f in files_in_zip: if f.lower().endswith('.pdf'): pdf_path = os.path.join(root, f) job_queue[job_id]['progress'] = f'Processing PDF: {f}' pdf_info = extract_names_from_pdf(pdf_path) if 'error' in pdf_info: processed_files.append(('skipped', f, pdf_info['error'])) job_queue[job_id]['progress'] = f'Skipped {f} - {pdf_info["error"]}' else: job_queue[job_id]['progress'] = f'Extracted data for {pdf_info["first_name"]} {pdf_info["last_name"]}' # 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(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, episode_dir), new_path)) processed_count += 1 job_queue[job_id]['progress'] = f'Organized {processed_count}/{total_files} files' else: # Single PDF job_queue[job_id]['progress'] = f'Processing PDF: {filename}' pdf_info = extract_names_from_pdf(filepath) if 'error' in pdf_info: processed_files.append(('skipped', filename, pdf_info['error'])) job_queue[job_id]['progress'] = f'Skipped {filename} - {pdf_info["error"]}' else: job_queue[job_id]['progress'] = f'Extracted data for {pdf_info["first_name"]} {pdf_info["last_name"]}' # 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(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.relpath(new_filepath, episode_dir), new_filepath)) processed_count += 1 job_queue[job_id]['progress'] = f'Organized {processed_count}/{total_files} files' job_queue[job_id]['progress'] = 'Creating ZIP archive...' # Create ZIP of the entire Episode directory structure successful_files = [p for p in processed_files if p[0] == 'success'] if successful_files: try: zip_output = os.path.join(app.config['UPLOAD_FOLDER'], f'renamed_{job_id}.zip') with zipfile.ZipFile(zip_output, 'w', zipfile.ZIP_DEFLATED) as zip_out: 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' job_queue[job_id]['progress'] = f'Created ZIP archive with {len(successful_files)} organized files' except Exception as e: job_queue[job_id]['progress'] = f'Error creating ZIP: {str(e)}' job_queue[job_id]['status'] = 'error' else: job_queue[job_id]['progress'] = 'No files to archive - all files were skipped' job_queue[job_id]['status'] = 'completed' job_queue[job_id]['processed_files'] = processed_files job_queue[job_id]['skipped'] = [(p[1], p[2]) for p in processed_files if p[0] == 'skipped'] @app.route('/login') def login(): return render_template_string('''
Status: {{ job.status.title() }}
{% if job.status == 'processing' and job.progress %}Progress: {{ job.progress }}
{% endif %}Files: {{ job.files|length }}
{% if job.zip_contents %}ZIP Contents:
{% for zip_name, pdf_files in job.zip_contents.items() %}No files were successfully processed to create a ZIP archive.
{% endif %} {% if job.skipped %}Skipped Files:
No jobs yet. Upload some files to get started.
{% endif %} ''', jobs=job_queue) @app.route('/download/