File size: 10,305 Bytes
0ff5e3d aa0d35c 0ff5e3d aa0d35c 0ff5e3d aa0d35c 0ff5e3d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 | 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.")
|