BuddyMath / report_generator.py
dotandru's picture
feat: Payment Page integration and Rebranding to ื”ืžื•ืจื” ืœืžืชืžื˜ื™ืงื”
aa0d35c
Raw
History Blame
10.3 kB
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.")