import os import json import urllib.parse from jinja2 import Environment, FileSystemLoader # Try to import pdfkit if available try: import pdfkit HAS_PDFKIT = True except ImportError: HAS_PDFKIT = False # Fallback to weasyprint if preferred/available try: from weasyprint import HTML HAS_WEASYPRINT = True except Exception as e: print(f"Weasyprint not available: {e}") HAS_WEASYPRINT = False import sendgrid from sendgrid.helpers.mail import Mail, Attachment, FileContent, FileName, FileType, Disposition import base64 from firebase_manager import firebase_manager class ReportGenerator: """ Generates weekly Parent Reports in HTML and PDF formats natively. """ def __init__(self): self.templates_dir = os.path.join(os.path.dirname(__file__), "templates") # Ensure templates directory exists os.makedirs(self.templates_dir, exist_ok=True) self.jinja_env = Environment(loader=FileSystemLoader(self.templates_dir)) def _get_db(self): return firebase_manager.get_db() def generate_quickchart_url(self, labels, data, title="מיומנויות"): """ Builds a URL for QuickChart.io to generate a static Radar Chart image. """ chart_config = { "type": "radar", "data": { "labels": labels, "datasets": [{ "label": title, "data": data, "backgroundColor": "rgba(59, 130, 246, 0.2)", "borderColor": "rgba(59, 130, 246, 1)", "pointBackgroundColor": "rgba(59, 130, 246, 1)", "pointBorderColor": "#fff", "pointHoverBackgroundColor": "#fff", "pointHoverBorderColor": "rgba(59, 130, 246, 1)" }] }, "options": { "scale": { "ticks": { "beginAtZero": True, "max": 100, "stepSize": 20 }, "pointLabels": { "fontSize": 14, "fontFamily": "sans-serif" } }, "legend": { "display": False } } } encoded_config = urllib.parse.quote(json.dumps(chart_config)) # Use high resolution for PDF printing return f"https://quickchart.io/chart?w=500&h=500&c={encoded_config}" def produce_weekly_report(self, uid: str, week_id: str, student_name: str = "התלמיד"): """ Fetches data from Firestore, renders the HTML template, and returns the HTML string. """ db = self._get_db() if not db: raise Exception("Firestore DB not initialized.") doc_ref = db.collection("users").document(uid).collection("analytics").document(week_id) doc = doc_ref.get() if not doc.exists: raise Exception(f"No analytics data found for {uid} in {week_id}") data = doc.to_dict() # Process Skills metrics skills_raw = data.get("skills_data", {}) skills_processed = [] chart_labels = [] chart_data = [] total_score_sum = 0 total_skills_count = 0 # Known taxonomy to ensure a consistent radar outline (even if 0) taxonomy = ["אלגברה", "גיאומטריה", "טריגונומטריה", "הסתברות", "חשבון דיפרנציאלי"] for skill_name in taxonomy: chart_labels.append(skill_name) if skill_name in skills_raw: skill_info = skills_raw[skill_name] s_sum = skill_info.get("sum_scores", 0) s_count = skill_info.get("count", 0) s_avg = int(s_sum / s_count) if s_count > 0 else 0 chart_data.append(s_avg) total_score_sum += s_avg total_skills_count += 1 skills_processed.append({ "name": skill_name, "score": s_avg, "sub_skills": skill_info.get("sub_skills", []) }) else: chart_data.append(0) overall_mastery = int(total_score_sum / total_skills_count) if total_skills_count > 0 else 0 total_exercises = data.get("total_exercises", 0) parent_notes = data.get("parent_notes", []) # Deduplicate parent notes and limit to top 5 parent_notes = list(set(parent_notes))[:5] # Identify "Focus for next week" (Lowest mastery score that is > 0 or first parent note) focus_for_next_week = "" if skills_processed: # Sort skills by score ascending sorted_skills = sorted(skills_processed, key=lambda x: x["score"]) lowest_skill = sorted_skills[0] if lowest_skill["score"] < 80: focus_target = ", ".join(lowest_skill['sub_skills']) if lowest_skill['sub_skills'] else lowest_skill['name'] focus_for_next_week = f"חזרה על {focus_target} לחיזוק השליטה בנושא." if not focus_for_next_week and parent_notes: focus_for_next_week = parent_notes[0] # Generate Chart chart_url = self.generate_quickchart_url(chart_labels, chart_data) # Template Rendering template = self.jinja_env.get_template("report_template.html") html_content = template.render( week_label=week_id.replace("week_", "").replace("_", "/"), student_name=student_name, total_exercises=total_exercises, overall_mastery=overall_mastery, skills=skills_processed, chart_url=chart_url, parent_notes=parent_notes, focus_for_next_week=focus_for_next_week ) return html_content def export_to_pdf(self, html_content: str, output_path: str): """ Converts the rendered HTML to a PDF file. Prefer WeasyPrint if available (better CSS/font support), fallback to pdfkit. """ if HAS_WEASYPRINT: HTML(string=html_content).write_pdf(output_path) return True elif HAS_PDFKIT: options = { 'page-size': 'A4', 'margin-top': '0.75in', 'margin-right': '0.75in', 'margin-bottom': '0.75in', 'margin-left': '0.75in', 'encoding': "UTF-8", 'no-outline': None } pdfkit.from_string(html_content, output_path, options=options) return True else: raise Exception("No PDF rendering library found. Please install 'weasyprint' or 'pdfkit'.") def send_report_email(self, parent_email: str, student_name: str, pdf_path: str): """ Sends the generated PDF report via SendGrid. """ sg_api_key = os.environ.get("SENDGRID_API_KEY") if not sg_api_key: print("⚠️ [EMAIL] SENDGRID_API_KEY not found in environment. Skipping email.") return False try: sg = sendgrid.SendGridAPIClient(api_key=sg_api_key) # The sender email must be verified in SendGrid from_email = os.environ.get("SENDGRID_FROM_EMAIL", "reports@buddymath.co.il") subject = f"איך עבר השבוע של {student_name} במתמטיקה? 📈 הדוח השבועי מ-המורה למתמטיקה בפנים." # Simple content content = "היי! מצורף דוח התקדמות שבועי המפרט את המיומנויות שתורגלו במהלך השבוע. נמשיך לתרגל ולהשתפר!\n\nצוות המורה למתמטיקה 👨‍🏫" message = Mail( from_email=from_email, to_emails=parent_email, subject=subject, plain_text_content=content ) # Attach PDF with open(pdf_path, 'rb') as f: data = f.read() encoded_file = base64.b64encode(data).decode() attachment = Attachment( FileContent(encoded_file), FileName(f"Math_Teacher_Weekly_Report_{student_name}.pdf"), FileType('application/pdf'), Disposition('attachment') ) message.attachment = attachment response = sg.send(message) print(f"📧 [EMAIL] Report sent to {parent_email}. Status code: {response.status_code}") return response.status_code in [200, 202] except Exception as e: print(f"❌ [EMAIL] SendGrid failed: {e}") return False # Global Instance report_generator = ReportGenerator() if __name__ == "__main__": # Test script locally with mockup data if running directly print("Running Report Generator Test...") import asyncio gen = ReportGenerator() labels = ["אלגברה", "גיאומטריה", "טריגונומטריה", "הסתברות", "חשבון דיפרנציאלי"] data = [85, 90, 60, 100, 40] url = gen.generate_quickchart_url(labels, data) print("QuickChart URL:", url) mock_html = gen.jinja_env.get_template("report_template.html").render( week_label="11/2026", student_name="דותן הרוש", total_exercises=14, overall_mastery=75, skills=[ {"name": "אלגברה", "score": 85, "sub_skills": ["משוואות ממעלה ראשונה", "חוקי חזקות"]}, {"name": "גיאומטריה", "score": 90, "sub_skills": ["חפיפת משולשים"]}, ], chart_url=url, parent_notes=["התלמיד גילה הבנה טובה בבידוד משתנים", "שליטה מעולה בחוקי חזקות!"], focus_for_next_week="חזרה על נוסחאות הכפל המקוצר לשיפור הדיוק האלגברי." ) with open("sample_report.html", "w", encoding="utf-8") as f: f.write(mock_html) print("Generated sample_report.html directly to root dir for browser inspection.")